From 11eabed4ef1009ef40319480270255337f3c4bdb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 20 Feb 2026 12:38:19 +1000 Subject: [PATCH 001/136] Begin WebGPU backend --- ImageSharp.Drawing.sln | 7 + .../Brushes/WebGpuBrushData.cs | 38 + .../ImageSharp.Drawing.WebGPU.csproj | 60 + .../Shaders/CompositeCoverageShader.cs | 101 + .../Shaders/CoverageRasterizationShader.cs | 117 + .../WebGPUDrawingBackend.cs | 2184 +++++++++++++++++ .../ImageSharp.Drawing.csproj | 6 + .../Processing/Backends/CoverageCompositor.cs | 71 + .../Backends/CoveragePreparationMode.cs | 20 + .../Processing/Backends/CpuDrawingBackend.cs | 333 --- .../Backends/DefaultDrawingBackend.cs | 581 +++++ .../Backends/DrawingCoverageHandle.cs | 51 + .../Processing/Backends/IDrawingBackend.cs | 111 +- src/ImageSharp.Drawing/Processing/Brush.cs | 6 +- .../Processing/BrushApplicator.cs | 15 +- .../Processing/DrawingCanvas{TPixel}.cs | 485 ++++ .../Processing/EllipticGradientBrush.cs | 11 +- .../Processing/GradientBrush.cs | 12 +- .../Processing/ImageBrush.cs | 16 +- .../Processing/LinearGradientBrush.cs | 12 +- .../Processing/PathGradientBrush.cs | 17 +- .../Processing/PatternBrush.cs | 17 +- .../Processors/Drawing/DrawPathProcessor.cs | 12 +- .../Processors/Drawing/FillPathProcessor.cs | 18 +- .../Drawing/FillPathProcessor{TPixel}.cs | 28 +- .../Drawing/FillProcessor{TPixel}.cs | 106 +- .../Text/DrawTextProcessor{TPixel}.cs | 120 +- .../Processors/Text/DrawingOperation.cs | 20 +- .../Processors/Text/RichTextGlyphRenderer.cs | 265 +- .../Processing/RadialGradientBrush.cs | 12 +- .../RasterizerDefaultsExtensions.cs | 28 +- .../Processing/RecolorBrush.cs | 45 +- .../Processing/RichTextOptions.cs | 20 +- .../Processing/SolidBrush.cs | 19 +- .../Processing/SweepGradientBrush.cs | 12 +- .../Shapes/Rasterization/PolygonScanner.cs | 31 +- .../Shapes/Rasterization/PolygonScanning.MD | 4 +- .../ImageSharp.Drawing.Tests.csproj | 3 +- .../Backends/SkiaCoverageDrawingBackend.cs | 269 ++ .../SkiaCoverageDrawingBackendTests.cs | 98 + .../Backends/WebGPUDrawingBackendTests.cs | 142 ++ .../Processing/FillPathProcessorTests.cs | 6 + .../RasterizerDefaultsExtensionsTests.cs | 64 +- 43 files changed, 4774 insertions(+), 819 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs create mode 100644 src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 74e8e1549..c7e333c09 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -337,6 +337,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -359,6 +361,10 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -386,6 +392,7 @@ Global {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} + {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs new file mode 100644 index 000000000..520dd93bb --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal enum WebGpuBrushKind : uint +{ + SolidColor = 0 +} + +internal readonly struct WebGpuBrushData +{ + public WebGpuBrushData(WebGpuBrushKind kind, Vector4 solidColor) + { + this.Kind = kind; + this.SolidColor = solidColor; + } + + public WebGpuBrushKind Kind { get; } + + public Vector4 SolidColor { get; } + + public static bool TryCreate(Brush brush, out WebGpuBrushData brushData) + { + Guard.NotNull(brush, nameof(brush)); + + if (brush is SolidBrush solidBrush) + { + brushData = new WebGpuBrushData(WebGpuBrushKind.SolidColor, solidBrush.Color.ToScaledVector4()); + return true; + } + + brushData = default; + return false; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj new file mode 100644 index 000000000..14b43d012 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -0,0 +1,60 @@ + + + + SixLabors.ImageSharp.Drawing.WebGPU + SixLabors.ImageSharp.Drawing.WebGPU + SixLabors.ImageSharp.Drawing.Processing.Backends + SixLabors.ImageSharp.Drawing.WebGPU + sixlabors.imagesharp.drawing.128.png + LICENSE + https://github.com/SixLabors/ImageSharp.Drawing/ + $(RepositoryUrl) + Image Draw Shape Path Font + An extension to ImageSharp that allows the drawing of images, paths, and text. + Debug;Release + true + true + + + false + + + + + 1.0 + + + + + enable + Nullable + + + + + + net8.0;net10.0 + + + + + net8.0 + + + + + + + + + + + + + + + + + + + diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs new file mode 100644 index 000000000..73379cae9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class CompositeCoverageShader +{ + public static ReadOnlySpan Code => + """ + struct CompositeParams { + source_offset_x: u32, + source_offset_y: u32, + destination_x: u32, + destination_y: u32, + destination_width: u32, + destination_height: u32, + target_width: u32, + target_height: u32, + + brush_kind: u32, + _pad0: u32, + _pad1: u32, + _pad2: u32, + + solid_brush_color: vec4, + blend_percentage: f32, + _pad3: f32, + _pad4: f32, + _pad5: f32, + }; + + @group(0) @binding(0) + var coverage: texture_2d; + + @group(0) @binding(1) + var params: CompositeParams; + + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) local: vec2, + }; + + @vertex + fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var vertices = array, 6>( + vec2(0.0, 0.0), + vec2(f32(params.destination_width), 0.0), + vec2(0.0, f32(params.destination_height)), + vec2(0.0, f32(params.destination_height)), + vec2(f32(params.destination_width), 0.0), + vec2(f32(params.destination_width), f32(params.destination_height))); + + let local = vertices[vertex_index]; + let pixel = vec2(f32(params.destination_x), f32(params.destination_y)) + local; + let ndc_x = (pixel.x / f32(params.target_width)) * 2.0 - 1.0; + let ndc_y = 1.0 - (pixel.y / f32(params.target_height)) * 2.0; + + var output: VertexOutput; + output.position = vec4(ndc_x, ndc_y, 0.0, 1.0); + output.local = local; + return output; + } + + fn sample_brush(_local: vec2) -> vec4 { + switch params.brush_kind { + case 0u: { + return params.solid_brush_color; + } + default: { + return vec4(0.0); + } + } + } + + @fragment + fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let local_x = u32(floor(input.local.x)); + let local_y = u32(floor(input.local.y)); + let source = vec2( + i32(params.source_offset_x + local_x), + i32(params.source_offset_y + local_y)); + + let coverage_value = textureLoad(coverage, source, 0).r; + if (coverage_value <= 0.0) { + discard; + } + + let brush = sample_brush(input.local); + if (brush.a <= 0.0) { + discard; + } + + let source_alpha = brush.a * coverage_value * params.blend_percentage; + if (source_alpha <= 0.0) { + discard; + } + + return vec4(brush.rgb * source_alpha, source_alpha); + } + """u8; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs new file mode 100644 index 000000000..f158b35cd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class CoverageRasterizationShader +{ + public static ReadOnlySpan Code => + """ + struct Edge { + x0: f32, + y0: f32, + x1: f32, + y1: f32, + }; + + struct CoverageParams { + edge_count: u32, + intersection_rule: u32, + antialias: u32, + _pad0: u32, + sample_origin_x: f32, + sample_origin_y: f32, + _pad1: f32, + _pad2: f32, + }; + + @group(0) @binding(0) + var edges: array; + + @group(0) @binding(1) + var params: CoverageParams; + + struct VertexOutput { + @builtin(position) position: vec4, + }; + + @vertex + fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var positions = array, 3>( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0)); + + var output: VertexOutput; + output.position = vec4(positions[vertex_index], 0.0, 1.0); + return output; + } + + fn is_inside(sample: vec2) -> bool { + var winding: i32 = 0; + var crossings: u32 = 0u; + + for (var i: u32 = 0u; i < params.edge_count; i = i + 1u) { + let edge = edges[i]; + if (edge.y0 == edge.y1) { + continue; + } + + let upward = (edge.y0 <= sample.y) && (edge.y1 > sample.y); + let downward = (edge.y0 > sample.y) && (edge.y1 <= sample.y); + if (!(upward || downward)) { + continue; + } + + let t = (sample.y - edge.y0) / (edge.y1 - edge.y0); + let x = edge.x0 + t * (edge.x1 - edge.x0); + if (x > sample.x) { + crossings = crossings + 1u; + if (upward) { + winding = winding + 1; + } else { + winding = winding - 1; + } + } + } + + if (params.intersection_rule == 0u) { + return (crossings & 1u) == 1u; + } + + return winding != 0; + } + + fn single_sample(pixel: vec2) -> f32 { + let sample = pixel + vec2(params.sample_origin_x, params.sample_origin_y); + return select(0.0, 1.0, is_inside(sample)); + } + + fn antialias_sample(pixel: vec2) -> f32 { + // Supersample a fixed grid around the configured sample origin. + // This produces smoother coverage than the previous 2x2 tap pattern. + let grid: u32 = 8u; + let inv_sample_count = 1.0 / f32(grid * grid); + let origin = vec2(params.sample_origin_x, params.sample_origin_y); + let base = origin - vec2(0.5, 0.5); + + var covered = 0.0; + for (var y: u32 = 0u; y < grid; y = y + 1u) { + let fy = (f32(y) + 0.5) / f32(grid); + for (var x: u32 = 0u; x < grid; x = x + 1u) { + let fx = (f32(x) + 0.5) / f32(grid); + covered = covered + select(0.0, 1.0, is_inside(pixel + base + vec2(fx, fy))); + } + } + + return covered * inv_sample_count; + } + + @fragment + fn fs_main(@builtin(position) position: vec4) -> @location(0) vec4 { + let pixel = floor(position.xy); + let coverage = select(single_sample(pixel), antialias_sample(pixel), params.antialias != 0u); + return vec4(coverage, 0.0, 0.0, 1.0); + } + """u8; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs new file mode 100644 index 000000000..b0fc6f67c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -0,0 +1,2184 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +#pragma warning disable SA1201 // Elements should appear in the correct order +internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable +{ + private const uint CompositeVertexCount = 6; + private const uint CoverageVertexCount = 3; + private const int CallbackTimeoutMilliseconds = 10_000; + + private static ReadOnlySpan EntryPointVertex => "vs_main\0"u8; + + private static ReadOnlySpan EntryPointFragment => "fs_main\0"u8; + + private readonly object gpuSync = new(); + private readonly ConcurrentDictionary preparedCoverage = new(); + private readonly DefaultDrawingBackend fallbackBackend; + + private int nextCoverageHandleId; + private bool isDisposed; + private WebGPU? webGpu; + private Wgpu? wgpuExtension; + private Instance* instance; + private Adapter* adapter; + private Device* device; + private Queue* queue; + private BindGroupLayout* compositeBindGroupLayout; + private PipelineLayout* compositePipelineLayout; + private RenderPipeline* compositePipeline; + private BindGroupLayout* coverageBindGroupLayout; + private PipelineLayout* coveragePipelineLayout; + private RenderPipeline* coveragePipeline; + + private int compositeSessionDepth; + private bool compositeSessionGpuActive; + private bool compositeSessionDirty; + private Buffer2DRegion compositeSessionTarget; + private Texture* compositeSessionTargetTexture; + private TextureView* compositeSessionTargetView; + private WgpuBuffer* compositeSessionReadbackBuffer; + private uint compositeSessionReadbackBytesPerRow; + private ulong compositeSessionReadbackByteCount; + private static readonly bool TraceEnabled = string.Equals( + Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), + "1", + StringComparison.Ordinal); + + public WebGPUDrawingBackend() => this.fallbackBackend = DefaultDrawingBackend.Instance; + + private static void Trace(string message) + { + if (TraceEnabled) + { + Console.Error.WriteLine($"[WebGPU] {message}"); + } + } + + public int PrepareCoverageCallCount { get; private set; } + + public int GpuPrepareCoverageCallCount { get; private set; } + + public int FallbackPrepareCoverageCallCount { get; private set; } + + public int CompositeCoverageCallCount { get; private set; } + + public int GpuCompositeCoverageCallCount { get; private set; } + + public int CpuCompositeCoverageCallCount { get; private set; } + + public int ReleaseCoverageCallCount { get; private set; } + + public bool IsGpuReady { get; private set; } + + public bool GpuInitializationAttempted { get; private set; } + + public string? LastGpuInitializationFailure { get; private set; } + + public int LiveCoverageCount => this.preparedCoverage.Count; + + public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + + if (this.compositeSessionDepth > 0) + { + this.compositeSessionDepth++; + return; + } + + this.compositeSessionDepth = 1; + this.compositeSessionGpuActive = false; + this.compositeSessionDirty = false; + + if (!CanUseGpuSession() || !this.TryEnsureGpuReady()) + { + return; + } + + Buffer2DRegion rgbaTarget = Unsafe.As, Buffer2DRegion>(ref target); + + lock (this.gpuSync) + { + if (!this.TryBeginCompositeSessionLocked(rgbaTarget)) + { + return; + } + + this.compositeSessionGpuActive = true; + } + } + + public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + + if (this.compositeSessionDepth <= 0) + { + return; + } + + this.compositeSessionDepth--; + if (this.compositeSessionDepth > 0) + { + return; + } + + lock (this.gpuSync) + { + Trace($"EndCompositeSession: gpuActive={this.compositeSessionGpuActive} dirty={this.compositeSessionDirty}"); + if (this.compositeSessionGpuActive && this.compositeSessionDirty) + { + this.TryFlushCompositeSessionLocked(); + } + + this.ReleaseCompositeSessionLocked(); + } + + this.compositeSessionGpuActive = false; + this.compositeSessionDirty = false; + } + + public void FillPath( + Configuration configuration, + Buffer2DRegion target, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + + if (!CanUseGpuSession()) + { + this.fallbackBackend.FillPath(configuration, target, path, brush, graphicsOptions, rasterizerOptions); + return; + } + + Rectangle localTargetBounds = new(0, 0, target.Width, target.Height); + Rectangle clippedInterest = Rectangle.Intersect(localTargetBounds, rasterizerOptions.Interest); + if (clippedInterest.Equals(Rectangle.Empty)) + { + return; + } + + RasterizerOptions clippedOptions = clippedInterest.Equals(rasterizerOptions.Interest) + ? rasterizerOptions + : new RasterizerOptions( + clippedInterest, + rasterizerOptions.IntersectionRule, + rasterizerOptions.RasterizationMode, + rasterizerOptions.SamplingOrigin); + + CoveragePreparationMode preparationMode = + this.SupportsCoverageComposition(brush, graphicsOptions) + ? CoveragePreparationMode.Default + : CoveragePreparationMode.Fallback; + + DrawingCoverageHandle coverageHandle = this.PrepareCoverage( + path, + clippedOptions, + configuration.MemoryAllocator, + preparationMode); + if (!coverageHandle.IsValid) + { + return; + } + + try + { + Buffer2DRegion compositeTarget = target.GetSubRegion(clippedInterest); + Rectangle brushBounds = Rectangle.Ceiling(path.Bounds); + + this.CompositeCoverage( + configuration, + compositeTarget, + coverageHandle, + Point.Empty, + brush, + graphicsOptions, + brushBounds); + } + finally + { + this.ReleaseCoverage(coverageHandle); + } + } + + public void FillRegion( + Configuration configuration, + Buffer2DRegion target, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle region) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(brush, nameof(brush)); + + if (!CanUseGpuSession()) + { + this.fallbackBackend.FillRegion(configuration, target, brush, graphicsOptions, region); + return; + } + + Rectangle localTargetBounds = new(0, 0, target.Width, target.Height); + Rectangle clippedRegion = Rectangle.Intersect(localTargetBounds, region); + if (clippedRegion.Equals(Rectangle.Empty)) + { + return; + } + + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + RasterizerOptions rasterizerOptions = new( + clippedRegion, + IntersectionRule.NonZero, + rasterizationMode, + RasterizerSamplingOrigin.PixelBoundary); + + RectangularPolygon fillShape = new( + clippedRegion.X, + clippedRegion.Y, + clippedRegion.Width, + clippedRegion.Height); + + this.FillPath( + configuration, + target, + fillShape, + brush, + graphicsOptions, + rasterizerOptions); + } + + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(brush, nameof(brush)); + + return CanUseGpuComposite(graphicsOptions) + && WebGpuBrushData.TryCreate(brush, out _) + && this.TryEnsureGpuReady() + && this.compositeSessionGpuActive; + } + + public DrawingCoverageHandle PrepareCoverage( + IPath path, + in RasterizerOptions rasterizerOptions, + MemoryAllocator allocator, + CoveragePreparationMode preparationMode) + { + this.ThrowIfDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + + this.PrepareCoverageCallCount++; + Size size = rasterizerOptions.Interest.Size; + if (size.Width <= 0 || size.Height <= 0) + { + return default; + } + + if (preparationMode == CoveragePreparationMode.Fallback) + { + return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); + } + + if (!this.TryEnsureGpuReady()) + { + return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); + } + + if (!TryBuildEdges(path, rasterizerOptions.Interest.Location, out EdgeData[]? edges) || edges.Length == 0) + { + return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); + } + + Texture* coverageTexture = null; + TextureView* coverageView = null; + lock (this.gpuSync) + { + if (!this.IsGpuReady || + this.webGpu is null || + this.device is null || + this.queue is null || + this.coveragePipeline is null || + this.coverageBindGroupLayout is null || + !this.TryRasterizeCoverageTextureLocked(edges, in rasterizerOptions, out coverageTexture, out coverageView)) + { + return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); + } + } + + int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); + CoverageEntry entry = new(size.Width, size.Height) + { + GpuCoverageTexture = coverageTexture, + GpuCoverageView = coverageView + }; + + if (!this.preparedCoverage.TryAdd(handleId, entry)) + { + lock (this.gpuSync) + { + this.ReleaseCoverageTextureLocked(entry); + } + + entry.Dispose(); + throw new InvalidOperationException("Failed to cache prepared coverage."); + } + + this.GpuPrepareCoverageCallCount++; + return new DrawingCoverageHandle(handleId); + } + + private DrawingCoverageHandle PrepareCoverageFallback( + IPath path, + in RasterizerOptions rasterizerOptions, + MemoryAllocator allocator) + { + this.FallbackPrepareCoverageCallCount++; + DrawingCoverageHandle fallbackHandle = this.fallbackBackend.PrepareCoverage( + path, + rasterizerOptions, + allocator, + CoveragePreparationMode.Fallback); + if (!fallbackHandle.IsValid) + { + return default; + } + + Size size = rasterizerOptions.Interest.Size; + int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); + CoverageEntry entry = new(size.Width, size.Height) + { + FallbackCoverageHandle = fallbackHandle + }; + + if (!this.preparedCoverage.TryAdd(handleId, entry)) + { + this.fallbackBackend.ReleaseCoverage(fallbackHandle); + throw new InvalidOperationException("Failed to cache prepared fallback coverage."); + } + + return new DrawingCoverageHandle(handleId); + } + + public void CompositeCoverage( + Configuration configuration, + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(brush, nameof(brush)); + this.CompositeCoverageCallCount++; + + if (!coverageHandle.IsValid) + { + return; + } + + if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out CoverageEntry? entry)) + { + throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); + } + + if (entry.IsFallback) + { + this.CpuCompositeCoverageCallCount++; + this.fallbackBackend.CompositeCoverage( + configuration, + target, + entry.FallbackCoverageHandle, + sourceOffset, + brush, + graphicsOptions, + brushBounds); + return; + } + + if (!CanUseGpuComposite(graphicsOptions) || + !WebGpuBrushData.TryCreate(brush, out WebGpuBrushData brushData) || + !this.TryEnsureGpuReady()) + { + throw new InvalidOperationException( + "Mixed-mode coverage composition is disabled. Coverage was prepared for accelerated composition, but the current composite settings are not GPU-supported."); + } + + Buffer2DRegion rgbaTarget = Unsafe.As, Buffer2DRegion>(ref target); + if (!this.TryCompositeCoverageGpu( + rgbaTarget, + coverageHandle, + sourceOffset, + brushData, + graphicsOptions.BlendPercentage)) + { + throw new InvalidOperationException( + "Accelerated coverage composition failed for a handle prepared for accelerated mode."); + } + + this.GpuCompositeCoverageCallCount++; + } + + public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) + { + this.ReleaseCoverageCallCount++; + if (!coverageHandle.IsValid) + { + return; + } + + Trace($"ReleaseCoverage: handle={coverageHandle.Value}"); + if (this.preparedCoverage.TryRemove(coverageHandle.Value, out CoverageEntry? entry)) + { + if (entry.IsFallback) + { + this.fallbackBackend.ReleaseCoverage(entry.FallbackCoverageHandle); + } + + lock (this.gpuSync) + { + this.ReleaseCoverageTextureLocked(entry); + } + + entry.Dispose(); + } + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + Trace("Dispose: begin"); + lock (this.gpuSync) + { + if (this.compositeSessionGpuActive && this.compositeSessionDirty) + { + this.TryFlushCompositeSessionLocked(); + } + + this.ReleaseCompositeSessionLocked(); + + foreach (KeyValuePair kv in this.preparedCoverage) + { + this.ReleaseCoverageTextureLocked(kv.Value); + kv.Value.Dispose(); + } + + this.preparedCoverage.Clear(); + this.ReleaseGpuResourcesLocked(); + } + + this.isDisposed = true; + Trace("Dispose: end"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseGpuComposite(in GraphicsOptions graphicsOptions) + where TPixel : unmanaged, IPixel + => typeof(TPixel) == typeof(Rgba32) + && graphicsOptions.AlphaCompositionMode == PixelAlphaCompositionMode.SrcOver + && graphicsOptions.ColorBlendingMode == PixelColorBlendingMode.Normal + && graphicsOptions.BlendPercentage > 0F; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseGpuSession() + where TPixel : unmanaged, IPixel + => typeof(TPixel) == typeof(Rgba32); + + private bool TryEnsureGpuReady() + { + if (this.IsGpuReady) + { + return true; + } + + lock (this.gpuSync) + { + if (this.IsGpuReady) + { + return true; + } + + if (this.GpuInitializationAttempted) + { + return false; + } + + this.GpuInitializationAttempted = true; + this.LastGpuInitializationFailure = null; + this.IsGpuReady = this.TryInitializeGpuLocked(); + return this.IsGpuReady; + } + } + + private bool TryInitializeGpuLocked() + { + Trace("TryInitializeGpuLocked: begin"); + try + { + this.webGpu = WebGPU.GetApi(); + _ = this.webGpu.TryGetDeviceExtension(null, out this.wgpuExtension); + Trace($"TryInitializeGpuLocked: extension={(this.wgpuExtension is null ? "none" : "wgpu.h")}"); + this.instance = this.webGpu.CreateInstance((InstanceDescriptor*)null); + if (this.instance is null) + { + this.LastGpuInitializationFailure = "WebGPU.CreateInstance returned null."; + Trace("TryInitializeGpuLocked: CreateInstance returned null"); + return false; + } + + Trace("TryInitializeGpuLocked: created instance"); + if (!this.TryRequestAdapterLocked(out this.adapter) || this.adapter is null) + { + this.LastGpuInitializationFailure ??= "Failed to request WebGPU adapter."; + Trace($"TryInitializeGpuLocked: request adapter failed ({this.LastGpuInitializationFailure})"); + return false; + } + + Trace("TryInitializeGpuLocked: adapter acquired"); + if (!this.TryRequestDeviceLocked(out this.device) || this.device is null) + { + this.LastGpuInitializationFailure ??= "Failed to request WebGPU device."; + Trace($"TryInitializeGpuLocked: request device failed ({this.LastGpuInitializationFailure})"); + return false; + } + + this.queue = this.webGpu.DeviceGetQueue(this.device); + if (this.queue is null) + { + this.LastGpuInitializationFailure = "WebGPU.DeviceGetQueue returned null."; + Trace("TryInitializeGpuLocked: DeviceGetQueue returned null"); + return false; + } + + Trace("TryInitializeGpuLocked: queue acquired"); + if (!this.TryCreateCompositePipelineLocked()) + { + this.LastGpuInitializationFailure = "Failed to create WebGPU composite pipeline."; + Trace("TryInitializeGpuLocked: composite pipeline creation failed"); + return false; + } + + Trace("TryInitializeGpuLocked: composite pipeline ready"); + if (!this.TryCreateCoveragePipelineLocked()) + { + this.LastGpuInitializationFailure = "Failed to create WebGPU coverage pipeline."; + Trace("TryInitializeGpuLocked: coverage pipeline creation failed"); + return false; + } + + Trace("TryInitializeGpuLocked: coverage pipeline ready"); + return true; + } + catch (Exception ex) + { + this.LastGpuInitializationFailure = $"WebGPU initialization threw: {ex.Message}"; + Trace($"TryInitializeGpuLocked: exception {ex}"); + return false; + } + finally + { + if (!this.IsGpuReady && + (this.compositePipeline is null || + this.compositePipelineLayout is null || + this.compositeBindGroupLayout is null || + this.coveragePipeline is null || + this.coveragePipelineLayout is null || + this.coverageBindGroupLayout is null || + this.device is null || + this.queue is null)) + { + this.LastGpuInitializationFailure ??= "WebGPU initialization left required resources unavailable."; + this.ReleaseGpuResourcesLocked(); + } + + Trace($"TryInitializeGpuLocked: end ready={this.IsGpuReady} error={this.LastGpuInitializationFailure ?? ""}"); + } + } + + private bool TryRequestAdapterLocked(out Adapter* resultAdapter) + { + resultAdapter = null; + if (this.webGpu is null || this.instance is null) + { + return false; + } + + RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; + Adapter* callbackAdapter = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* messagePtr, void* userDataPtr) + { + callbackStatus = status; + callbackAdapter = adapterPtr; + _ = messagePtr; + _ = userDataPtr; + callbackReady.Set(); + } + + using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); + RequestAdapterOptions options = new() + { + PowerPreference = PowerPreference.HighPerformance + }; + + this.webGpu.InstanceRequestAdapter(this.instance, in options, callbackPtr, null); + if (!this.WaitForSignalLocked(callbackReady)) + { + this.LastGpuInitializationFailure = "Timed out while waiting for WebGPU adapter request callback."; + Trace("TryRequestAdapterLocked: timeout waiting for callback"); + return false; + } + + resultAdapter = callbackAdapter; + if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) + { + this.LastGpuInitializationFailure = $"WebGPU adapter request failed with status '{callbackStatus}'."; + Trace($"TryRequestAdapterLocked: callback status={callbackStatus} adapter={(nint)callbackAdapter:X}"); + return false; + } + + return true; + } + + private bool TryRequestDeviceLocked(out Device* resultDevice) + { + resultDevice = null; + if (this.webGpu is null || this.adapter is null) + { + return false; + } + + RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; + Device* callbackDevice = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, void* userDataPtr) + { + callbackStatus = status; + callbackDevice = devicePtr; + _ = messagePtr; + _ = userDataPtr; + callbackReady.Set(); + } + + using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); + DeviceDescriptor descriptor = default; + this.webGpu.AdapterRequestDevice(this.adapter, in descriptor, callbackPtr, null); + + if (!this.WaitForSignalLocked(callbackReady)) + { + this.LastGpuInitializationFailure = "Timed out while waiting for WebGPU device request callback."; + Trace("TryRequestDeviceLocked: timeout waiting for callback"); + return false; + } + + resultDevice = callbackDevice; + if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) + { + this.LastGpuInitializationFailure = $"WebGPU device request failed with status '{callbackStatus}'."; + Trace($"TryRequestDeviceLocked: callback status={callbackStatus} device={(nint)callbackDevice:X}"); + return false; + } + + return true; + } + + private bool TryCreateCompositePipelineLocked() + { + if (this.webGpu is null || this.device is null) + { + return false; + } + + BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; + layoutEntries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + layoutEntries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Vertex | ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + MinBindingSize = (ulong)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor layoutDescriptor = new() + { + EntryCount = 2, + Entries = layoutEntries + }; + + this.compositeBindGroupLayout = this.webGpu.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); + if (this.compositeBindGroupLayout is null) + { + return false; + } + + BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; + bindGroupLayouts[0] = this.compositeBindGroupLayout; + PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + BindGroupLayoutCount = 1, + BindGroupLayouts = bindGroupLayouts + }; + + this.compositePipelineLayout = this.webGpu.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); + if (this.compositePipelineLayout is null) + { + return false; + } + + ShaderModule* shaderModule = null; + try + { + ReadOnlySpan shaderCode = CompositeCoverageShader.Code; + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct + { + SType = SType.ShaderModuleWgslDescriptor + }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + shaderModule = this.webGpu.DeviceCreateShaderModule(this.device, in shaderDescriptor); + } + + if (shaderModule is null) + { + return false; + } + + ReadOnlySpan vertexEntryPoint = EntryPointVertex; + ReadOnlySpan fragmentEntryPoint = EntryPointFragment; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) + { + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + { + VertexState vertexState = new() + { + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + BlendState blendState = new() + { + Color = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + }, + Alpha = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + } + }; + + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = TextureFormat.Rgba8Unorm, + Blend = &blendState, + WriteMask = ColorWriteMask.All + }; + + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; + + RenderPipelineDescriptor pipelineDescriptor = new() + { + Layout = this.compositePipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState + }; + + this.compositePipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + } + } + + return this.compositePipeline is not null; + } + finally + { + if (shaderModule is not null) + { + this.webGpu.ShaderModuleRelease(shaderModule); + } + } + } + + private bool TryCreateCoveragePipelineLocked() + { + if (this.webGpu is null || this.device is null) + { + return false; + } + + BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; + layoutEntries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + MinBindingSize = 16 + } + }; + layoutEntries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + MinBindingSize = (ulong)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor layoutDescriptor = new() + { + EntryCount = 2, + Entries = layoutEntries + }; + + this.coverageBindGroupLayout = this.webGpu.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); + if (this.coverageBindGroupLayout is null) + { + return false; + } + + BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; + bindGroupLayouts[0] = this.coverageBindGroupLayout; + PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + BindGroupLayoutCount = 1, + BindGroupLayouts = bindGroupLayouts + }; + + this.coveragePipelineLayout = this.webGpu.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); + if (this.coveragePipelineLayout is null) + { + return false; + } + + ShaderModule* shaderModule = null; + try + { + ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct + { + SType = SType.ShaderModuleWgslDescriptor + }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + shaderModule = this.webGpu.DeviceCreateShaderModule(this.device, in shaderDescriptor); + } + + if (shaderModule is null) + { + return false; + } + + ReadOnlySpan vertexEntryPoint = EntryPointVertex; + ReadOnlySpan fragmentEntryPoint = EntryPointFragment; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) + { + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + { + VertexState vertexState = new() + { + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = TextureFormat.R8Unorm, + Blend = null, + WriteMask = ColorWriteMask.Red + }; + + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; + + RenderPipelineDescriptor pipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState + }; + + this.coveragePipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + } + } + + return this.coveragePipeline is not null; + } + finally + { + if (shaderModule is not null) + { + this.webGpu.ShaderModuleRelease(shaderModule); + } + } + } + + private bool TryRasterizeCoverageTextureLocked( + ReadOnlySpan edges, + in RasterizerOptions rasterizerOptions, + out Texture* coverageTexture, + out TextureView* coverageView) + { + Trace($"TryRasterizeCoverageTextureLocked: begin edges={edges.Length} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); + coverageTexture = null; + coverageView = null; + + if (this.webGpu is null || + this.device is null || + this.queue is null || + this.coveragePipeline is null || + this.coverageBindGroupLayout is null || + edges.Length == 0 || + rasterizerOptions.Interest.Width <= 0 || + rasterizerOptions.Interest.Height <= 0) + { + return false; + } + + TextureDescriptor coverageTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = 1 + }; + + coverageTexture = this.webGpu.DeviceCreateTexture(this.device, in coverageTextureDescriptor); + if (coverageTexture is null) + { + return false; + } + + TextureViewDescriptor coverageViewDescriptor = new() + { + Format = TextureFormat.R8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + coverageView = this.webGpu.TextureCreateView(coverageTexture, in coverageViewDescriptor); + if (coverageView is null) + { + this.ReleaseTextureLocked(coverageTexture); + coverageTexture = null; + return false; + } + + ulong edgesBufferSize = checked((ulong)edges.Length * (ulong)Unsafe.SizeOf()); + ulong paramsBufferSize = (ulong)Unsafe.SizeOf(); + WgpuBuffer* edgesBuffer = null; + WgpuBuffer* paramsBuffer = null; + BindGroup* bindGroup = null; + CommandEncoder* commandEncoder = null; + RenderPassEncoder* passEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor edgesBufferDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = edgesBufferSize + }; + edgesBuffer = this.webGpu.DeviceCreateBuffer(this.device, in edgesBufferDescriptor); + if (edgesBuffer is null) + { + return false; + } + + BufferDescriptor paramsBufferDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = paramsBufferSize + }; + paramsBuffer = this.webGpu.DeviceCreateBuffer(this.device, in paramsBufferDescriptor); + if (paramsBuffer is null) + { + return false; + } + + fixed (EdgeData* edgesPtr = edges) + { + this.webGpu.QueueWriteBuffer(this.queue, edgesBuffer, 0, edgesPtr, (nuint)edgesBufferSize); + } + + CoverageParams coverageParams = new() + { + EdgeCount = (uint)edges.Length, + IntersectionRule = rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 0U : 1U, + Antialias = rasterizerOptions.RasterizationMode == RasterizationMode.Antialiased ? 1U : 0U, + SampleOriginX = rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F, + SampleOriginY = rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F + }; + this.webGpu.QueueWriteBuffer( + this.queue, + paramsBuffer, + 0, + ref coverageParams, + (nuint)Unsafe.SizeOf()); + + BindGroupEntry* bindEntries = stackalloc BindGroupEntry[2]; + bindEntries[0] = new BindGroupEntry + { + Binding = 0, + Buffer = edgesBuffer, + Offset = 0, + Size = edgesBufferSize + }; + bindEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = paramsBuffer, + Offset = 0, + Size = paramsBufferSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = this.coverageBindGroupLayout, + EntryCount = 2, + Entries = bindEntries + }; + bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); + if (bindGroup is null) + { + return false; + } + + CommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + RenderPassColorAttachment colorAttachment = new() + { + View = coverageView, + ResolveTarget = null, + LoadOp = LoadOp.Clear, + StoreOp = StoreOp.Store, + ClearValue = default + }; + + RenderPassDescriptor renderPassDescriptor = new() + { + ColorAttachmentCount = 1, + ColorAttachments = &colorAttachment + }; + + passEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + if (passEncoder is null) + { + return false; + } + + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coveragePipeline); + this.webGpu.RenderPassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, (uint*)null); + this.webGpu.RenderPassEncoderDraw(passEncoder, CoverageVertexCount, 1, 0, 0); + this.webGpu.RenderPassEncoderEnd(passEncoder); + this.webGpu.RenderPassEncoderRelease(passEncoder); + passEncoder = null; + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); + if (this.wgpuExtension is not null) + { + _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); + } + + this.webGpu.CommandBufferRelease(commandBuffer); + commandBuffer = null; + Trace("TryRasterizeCoverageTextureLocked: submitted"); + return true; + } + finally + { + if (passEncoder is not null) + { + this.webGpu.RenderPassEncoderRelease(passEncoder); + } + + if (commandBuffer is not null) + { + this.webGpu.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.webGpu.CommandEncoderRelease(commandEncoder); + } + + if (bindGroup is not null) + { + this.webGpu.BindGroupRelease(bindGroup); + } + + this.ReleaseBufferLocked(paramsBuffer); + this.ReleaseBufferLocked(edgesBuffer); + } + } + + private static bool TryBuildEdges(IPath path, Point interestLocation, [NotNullWhen(true)] out EdgeData[]? edges) + { + List edgeList = []; + float offsetX = -interestLocation.X; + float offsetY = -interestLocation.Y; + + foreach (ISimplePath simplePath in path.Flatten()) + { + ReadOnlySpan points = simplePath.Points.Span; + if (points.Length < 2) + { + continue; + } + + for (int i = 1; i < points.Length; i++) + { + AddEdge(points[i - 1], points[i], offsetX, offsetY, edgeList); + } + + if (simplePath.IsClosed) + { + AddEdge(points[^1], points[0], offsetX, offsetY, edgeList); + } + } + + if (edgeList.Count == 0) + { + edges = null; + return false; + } + + edges = [.. edgeList]; + return true; + } + + private static void AddEdge(PointF from, PointF to, float offsetX, float offsetY, List destination) + { + if (from.Equals(to)) + { + return; + } + + destination.Add(new EdgeData + { + X0 = from.X + offsetX, + Y0 = from.Y + offsetY, + X1 = to.X + offsetX, + Y1 = to.Y + offsetY + }); + } + + private bool WaitForSignalLocked(ManualResetEventSlim signal) + { + Stopwatch timer = Stopwatch.StartNew(); + while (!signal.Wait(1)) + { + if (timer.ElapsedMilliseconds >= CallbackTimeoutMilliseconds) + { + return false; + } + + if (this.wgpuExtension is not null && this.device is not null) + { + _ = this.wgpuExtension.DevicePoll(this.device, false, (WrappedSubmissionIndex*)null); + continue; + } + + if (this.instance is not null && this.webGpu is not null) + { + this.webGpu.InstanceProcessEvents(this.instance); + } + } + + return true; + } + + private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) + { + if (this.webGpu is null || this.queue is null || destinationTexture is null) + { + return false; + } + + int pixelSizeInBytes = Unsafe.SizeOf(); + ImageCopyTexture destination = new() + { + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + + if (IsSingleMemory(sourceRegion.Buffer)) + { + int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); + int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)sourceStrideBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + Span firstRow = sourceRegion.DangerousGetRowSpan(0); + fixed (Rgba32* uploadPtr = firstRow) + { + this.webGpu.QueueWriteTexture(this.queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); + } + + return true; + } + + int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + int packedByteCount = checked(packedRowBytes * sourceRegion.Height); + byte[] rented = ArrayPool.Shared.Rent(packedByteCount); + try + { + Span packedData = rented.AsSpan(0, packedByteCount); + for (int y = 0; y < sourceRegion.Height; y++) + { + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); + } + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)packedRowBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + fixed (byte* uploadPtr = packedData) + { + this.webGpu.QueueWriteTexture(this.queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); + } + + return true; + } + catch + { + return false; + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + private bool TryBeginCompositeSessionLocked(Buffer2DRegion target) + { + this.ReleaseCompositeSessionLocked(); + + if (!this.IsGpuReady || + this.webGpu is null || + this.device is null || + this.queue is null || + target.Width <= 0 || + target.Height <= 0) + { + return false; + } + + uint textureRowBytes = checked((uint)target.Width * (uint)Unsafe.SizeOf()); + uint readbackRowBytes = AlignTo256(textureRowBytes); + ulong readbackByteCount = (ulong)readbackRowBytes * (uint)target.Height; + + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)target.Width, (uint)target.Height, 1), + Format = TextureFormat.Rgba8Unorm, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* targetTexture = this.webGpu.DeviceCreateTexture(this.device, in targetTextureDescriptor); + if (targetTexture is null) + { + return false; + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = TextureFormat.Rgba8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* targetView = this.webGpu.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + this.ReleaseTextureLocked(targetTexture); + return false; + } + + BufferDescriptor readbackBufferDescriptor = new() + { + Usage = BufferUsage.MapRead | BufferUsage.CopyDst, + Size = readbackByteCount + }; + + WgpuBuffer* readbackBuffer = this.webGpu.DeviceCreateBuffer(this.device, in readbackBufferDescriptor); + if (readbackBuffer is null) + { + this.ReleaseBufferLocked(readbackBuffer); + this.ReleaseTextureViewLocked(targetView); + this.ReleaseTextureLocked(targetTexture); + return false; + } + + if (!this.TryQueueWriteTextureFromRgbaRegionLocked(targetTexture, target)) + { + this.ReleaseBufferLocked(readbackBuffer); + this.ReleaseTextureViewLocked(targetView); + this.ReleaseTextureLocked(targetTexture); + return false; + } + + this.compositeSessionTarget = target; + this.compositeSessionTargetTexture = targetTexture; + this.compositeSessionTargetView = targetView; + this.compositeSessionReadbackBuffer = readbackBuffer; + this.compositeSessionReadbackBytesPerRow = readbackRowBytes; + this.compositeSessionReadbackByteCount = readbackByteCount; + return true; + } + + private bool TryFlushCompositeSessionLocked() + { + Trace("TryFlushCompositeSessionLocked: begin"); + if (this.webGpu is null || + this.device is null || + this.queue is null || + this.compositeSessionTargetTexture is null || + this.compositeSessionReadbackBuffer is null || + this.compositeSessionTarget.Width <= 0 || + this.compositeSessionTarget.Height <= 0 || + this.compositeSessionReadbackByteCount == 0 || + this.compositeSessionReadbackBytesPerRow == 0) + { + return false; + } + + CommandEncoder* commandEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + CommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + ImageCopyTexture source = new() + { + Texture = this.compositeSessionTargetTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + ImageCopyBuffer destination = new() + { + Buffer = this.compositeSessionReadbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = this.compositeSessionReadbackBytesPerRow, + RowsPerImage = (uint)this.compositeSessionTarget.Height + } + }; + + Extent3D copySize = new((uint)this.compositeSessionTarget.Width, (uint)this.compositeSessionTarget.Height, 1); + this.webGpu.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); + this.webGpu.CommandBufferRelease(commandBuffer); + commandBuffer = null; + + if (!this.TryReadBackBufferToRgbaRegionLocked( + this.compositeSessionReadbackBuffer, + checked((int)this.compositeSessionReadbackBytesPerRow), + this.compositeSessionTarget)) + { + Trace("TryFlushCompositeSessionLocked: readback failed"); + return false; + } + + Trace("TryFlushCompositeSessionLocked: completed"); + return true; + } + finally + { + if (commandBuffer is not null) + { + this.webGpu.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.webGpu.CommandEncoderRelease(commandEncoder); + } + } + } + + private void ReleaseCompositeSessionLocked() + { + this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); + this.ReleaseTextureViewLocked(this.compositeSessionTargetView); + this.ReleaseTextureLocked(this.compositeSessionTargetTexture); + this.compositeSessionReadbackBuffer = null; + this.compositeSessionTargetTexture = null; + this.compositeSessionTargetView = null; + this.compositeSessionReadbackBytesPerRow = 0; + this.compositeSessionReadbackByteCount = 0; + this.compositeSessionTarget = default; + this.compositeSessionDirty = false; + } + + private bool TryCompositeCoverageGpu( + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + WebGpuBrushData brushData, + float blendPercentage) + { + if (!coverageHandle.IsValid) + { + return true; + } + + if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out CoverageEntry? entry)) + { + throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); + } + + if (entry.IsFallback) + { + return false; + } + + if (target.Width <= 0 || target.Height <= 0) + { + return true; + } + + if ((uint)sourceOffset.X >= (uint)entry.Width || (uint)sourceOffset.Y >= (uint)entry.Height) + { + return true; + } + + int compositeWidth = Math.Min(target.Width, entry.Width - sourceOffset.X); + int compositeHeight = Math.Min(target.Height, entry.Height - sourceOffset.Y); + if (compositeWidth <= 0 || compositeHeight <= 0) + { + return true; + } + + Buffer2DRegion destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); + + lock (this.gpuSync) + { + if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || + this.compositePipeline is null || this.compositeBindGroupLayout is null || this.compositeSessionTargetView is null) + { + return false; + } + + if (!TryEnsureCoverageTextureLocked(entry)) + { + return false; + } + + if (this.compositeSessionGpuActive && + this.compositeSessionTargetTexture is not null && + this.compositeSessionTargetView is not null) + { + int destinationX = destinationRegion.Rectangle.X - this.compositeSessionTarget.Rectangle.X; + int destinationY = destinationRegion.Rectangle.Y - this.compositeSessionTarget.Rectangle.Y; + if ((uint)destinationX >= (uint)this.compositeSessionTarget.Width || + (uint)destinationY >= (uint)this.compositeSessionTarget.Height) + { + return false; + } + + int sessionCompositeWidth = Math.Min(compositeWidth, this.compositeSessionTarget.Width - destinationX); + int sessionCompositeHeight = Math.Min(compositeHeight, this.compositeSessionTarget.Height - destinationY); + if (sessionCompositeWidth <= 0 || sessionCompositeHeight <= 0) + { + return true; + } + + if (this.TryRunCompositePassInSessionLocked( + entry, + sourceOffset, + brushData, + blendPercentage, + destinationX, + destinationY, + sessionCompositeWidth, + sessionCompositeHeight)) + { + this.compositeSessionDirty = true; + return true; + } + + if (this.compositeSessionDirty) + { + this.TryFlushCompositeSessionLocked(); + } + + this.ReleaseCompositeSessionLocked(); + this.compositeSessionGpuActive = false; + return false; + } + + return false; + } + } + + private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) + { + if (entry.GpuCoverageTexture is not null && entry.GpuCoverageView is not null) + { + return true; + } + + return false; + } + + private bool TryRunCompositePassInSessionLocked( + CoverageEntry coverageEntry, + Point sourceOffset, + WebGpuBrushData brushData, + float blendPercentage, + int destinationX, + int destinationY, + int compositeWidth, + int compositeHeight) + { + if (this.webGpu is null || + this.device is null || + this.queue is null || + this.compositePipeline is null || + this.compositeBindGroupLayout is null || + coverageEntry.GpuCoverageView is null || + this.compositeSessionTargetView is null) + { + return false; + } + + if (compositeWidth <= 0 || compositeHeight <= 0) + { + return true; + } + + ulong uniformByteCount = (ulong)Unsafe.SizeOf(); + WgpuBuffer* uniformBuffer = null; + BindGroup* bindGroup = null; + CommandEncoder* commandEncoder = null; + RenderPassEncoder* passEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor uniformBufferDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = uniformByteCount + }; + uniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); + if (uniformBuffer is null) + { + return false; + } + + CompositeParams parameters = new() + { + SourceOffsetX = (uint)sourceOffset.X, + SourceOffsetY = (uint)sourceOffset.Y, + DestinationX = (uint)destinationX, + DestinationY = (uint)destinationY, + DestinationWidth = (uint)compositeWidth, + DestinationHeight = (uint)compositeHeight, + TargetWidth = (uint)this.compositeSessionTarget.Width, + TargetHeight = (uint)this.compositeSessionTarget.Height, + BrushKind = (uint)brushData.Kind, + SolidBrushColor = brushData.SolidColor, + BlendPercentage = blendPercentage + }; + + this.webGpu.QueueWriteBuffer( + this.queue, + uniformBuffer, + 0, + ref parameters, + (nuint)Unsafe.SizeOf()); + + BindGroupEntry* bindEntries = stackalloc BindGroupEntry[2]; + bindEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageEntry.GpuCoverageView + }; + bindEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = uniformBuffer, + Offset = 0, + Size = uniformByteCount + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = this.compositeBindGroupLayout, + EntryCount = 2, + Entries = bindEntries + }; + bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); + if (bindGroup is null) + { + return false; + } + + CommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + RenderPassColorAttachment colorAttachment = new() + { + View = this.compositeSessionTargetView, + ResolveTarget = null, + LoadOp = LoadOp.Load, + StoreOp = StoreOp.Store, + ClearValue = default + }; + + RenderPassDescriptor renderPassDescriptor = new() + { + ColorAttachmentCount = 1, + ColorAttachments = &colorAttachment + }; + + passEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + if (passEncoder is null) + { + return false; + } + + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.compositePipeline); + this.webGpu.RenderPassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, (uint*)null); + this.webGpu.RenderPassEncoderDraw(passEncoder, CompositeVertexCount, 1, 0, 0); + this.webGpu.RenderPassEncoderEnd(passEncoder); + this.webGpu.RenderPassEncoderRelease(passEncoder); + passEncoder = null; + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); + if (this.wgpuExtension is not null) + { + _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); + } + + this.webGpu.CommandBufferRelease(commandBuffer); + commandBuffer = null; + return true; + } + finally + { + if (passEncoder is not null) + { + this.webGpu.RenderPassEncoderRelease(passEncoder); + } + + if (commandBuffer is not null) + { + this.webGpu.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.webGpu.CommandEncoderRelease(commandEncoder); + } + + if (bindGroup is not null) + { + this.webGpu.BindGroupRelease(bindGroup); + } + + this.ReleaseBufferLocked(uniformBuffer); + } + } + + private bool TryMapReadBufferLocked(WgpuBuffer* readbackBuffer, nuint byteCount, out byte* mappedData) + { + mappedData = null; + + if (this.webGpu is null || readbackBuffer is null) + { + return false; + } + + Trace($"TryReadBackBufferLocked: begin bytes={byteCount}"); + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim callbackReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userDataPtr) + { + mapStatus = status; + _ = userDataPtr; + callbackReady.Set(); + } + + using PfnBufferMapCallback callbackPtr = PfnBufferMapCallback.From(Callback); + this.webGpu.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); + + if (!this.WaitForSignalLocked(callbackReady) || mapStatus != BufferMapAsyncStatus.Success) + { + Trace($"TryReadBackBufferLocked: map failed status={mapStatus}"); + return false; + } + + Trace("TryReadBackBufferLocked: map callback success"); + void* rawMappedData = this.webGpu.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); + if (rawMappedData is null) + { + this.webGpu.BufferUnmap(readbackBuffer); + Trace("TryReadBackBufferLocked: mapped range null"); + return false; + } + + mappedData = (byte*)rawMappedData; + return true; + } + + private bool TryReadBackBufferToRgbaRegionLocked( + WgpuBuffer* readbackBuffer, + int sourceRowBytes, + Buffer2DRegion destinationRegion) + { + if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) + { + return true; + } + + int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); + int readbackByteCount = checked(sourceRowBytes * destinationRegion.Height); + if (!this.TryMapReadBufferLocked(readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) + { + return false; + } + + try + { + ReadOnlySpan sourceData = new(mappedData, readbackByteCount); + int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); + + // If the target region spans full rows in a contiguous backing buffer we can copy + // the mapped data in one block instead of per-row. + if (destinationRegion.Rectangle.X == 0 && + sourceRowBytes == destinationStrideBytes && + TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) + { + Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); + int destinationStart = checked(destinationRegion.Rectangle.Y * destinationStrideBytes); + int copyByteCount = checked(destinationStrideBytes * destinationRegion.Height); + if (destinationBytes.Length >= destinationStart + copyByteCount) + { + sourceData[..copyByteCount].CopyTo(destinationBytes.Slice(destinationStart, copyByteCount)); + return true; + } + } + + for (int y = 0; y < destinationRegion.Height; y++) + { + ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); + MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); + } + + return true; + } + finally + { + this.webGpu?.BufferUnmap(readbackBuffer); + + Trace("TryReadBackBufferLocked: completed"); + } + } + + private void ReleaseCoverageTextureLocked(CoverageEntry entry) + { + Trace($"ReleaseCoverageTextureLocked: tex={(nint)entry.GpuCoverageTexture:X} view={(nint)entry.GpuCoverageView:X}"); + this.ReleaseTextureViewLocked(entry.GpuCoverageView); + this.ReleaseTextureLocked(entry.GpuCoverageTexture); + entry.GpuCoverageView = null; + entry.GpuCoverageTexture = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSingleMemory(Buffer2D buffer) + where T : struct + => buffer.MemoryGroup.Count == 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memory) + where T : struct + { + if (!IsSingleMemory(buffer)) + { + memory = default; + return false; + } + + memory = buffer.MemoryGroup[0]; + return true; + } + + private void ReleaseTextureViewLocked(TextureView* textureView) + { + if (textureView is null || this.webGpu is null) + { + return; + } + + this.webGpu.TextureViewRelease(textureView); + } + + private void ReleaseTextureLocked(Texture* texture) + { + if (texture is null || this.webGpu is null) + { + return; + } + + this.webGpu.TextureRelease(texture); + } + + private void ReleaseBufferLocked(WgpuBuffer* buffer) + { + if (buffer is null || this.webGpu is null) + { + return; + } + + this.webGpu.BufferRelease(buffer); + } + + private void ReleaseGpuResourcesLocked() + { + Trace("ReleaseGpuResourcesLocked: begin"); + this.ReleaseCompositeSessionLocked(); + + if (this.webGpu is not null) + { + if (this.coveragePipeline is not null) + { + this.webGpu.RenderPipelineRelease(this.coveragePipeline); + this.coveragePipeline = null; + } + + if (this.coveragePipelineLayout is not null) + { + this.webGpu.PipelineLayoutRelease(this.coveragePipelineLayout); + this.coveragePipelineLayout = null; + } + + if (this.coverageBindGroupLayout is not null) + { + this.webGpu.BindGroupLayoutRelease(this.coverageBindGroupLayout); + this.coverageBindGroupLayout = null; + } + + if (this.compositePipeline is not null) + { + this.webGpu.RenderPipelineRelease(this.compositePipeline); + this.compositePipeline = null; + } + + if (this.compositePipelineLayout is not null) + { + this.webGpu.PipelineLayoutRelease(this.compositePipelineLayout); + this.compositePipelineLayout = null; + } + + if (this.compositeBindGroupLayout is not null) + { + this.webGpu.BindGroupLayoutRelease(this.compositeBindGroupLayout); + this.compositeBindGroupLayout = null; + } + + if (this.queue is not null) + { + this.webGpu.QueueRelease(this.queue); + this.queue = null; + } + + if (this.device is not null) + { + this.webGpu.DeviceRelease(this.device); + this.device = null; + } + + if (this.adapter is not null) + { + this.webGpu.AdapterRelease(this.adapter); + this.adapter = null; + } + + if (this.instance is not null) + { + this.webGpu.InstanceRelease(this.instance); + this.instance = null; + } + + this.webGpu.Dispose(); + this.webGpu = null; + } + + this.IsGpuReady = false; + this.compositeSessionGpuActive = false; + this.compositeSessionDepth = 0; + Trace("ReleaseGpuResourcesLocked: end"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); + + [StructLayout(LayoutKind.Sequential)] + private struct CompositeParams + { + public uint SourceOffsetX; + public uint SourceOffsetY; + public uint DestinationX; + public uint DestinationY; + public uint DestinationWidth; + public uint DestinationHeight; + public uint TargetWidth; + public uint TargetHeight; + public uint BrushKind; + public uint Padding0; + public uint Padding1; + public uint Padding2; + public Vector4 SolidBrushColor; + public float BlendPercentage; + public float Padding3; + public float Padding4; + public float Padding5; + } + + [StructLayout(LayoutKind.Sequential)] + private struct CoverageParams + { + public uint EdgeCount; + public uint IntersectionRule; + public uint Antialias; + public uint Padding0; + public float SampleOriginX; + public float SampleOriginY; + public float Padding1; + public float Padding2; + } + + [StructLayout(LayoutKind.Sequential)] + private struct EdgeData + { + public float X0; + public float Y0; + public float X1; + public float Y1; + } + + private sealed class CoverageEntry : IDisposable + { + public CoverageEntry(int width, int height) + { + this.Width = width; + this.Height = height; + } + + public int Width { get; } + + public int Height { get; } + + public DrawingCoverageHandle FallbackCoverageHandle { get; set; } + + public bool IsFallback => this.FallbackCoverageHandle.IsValid; + + public Texture* GpuCoverageTexture { get; set; } + + public TextureView* GpuCoverageView { get; set; } + + public void Dispose() + { + } + } +} diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 153c102b7..338b613d1 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -38,6 +38,11 @@ + + + + + @@ -48,5 +53,6 @@ + diff --git a/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs b/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs new file mode 100644 index 000000000..f8ee17f36 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Shared CPU compositing helpers for prepared coverage maps. +/// +internal static class CoverageCompositor +{ + public static bool TryGetCompositeRegions( + Buffer2DRegion target, + Buffer2D sourceBuffer, + Point sourceOffset, + out Buffer2DRegion destinationRegion, + out Buffer2DRegion sourceRegion) + where TPixel : unmanaged, IPixel + where TCoverage : unmanaged + { + destinationRegion = default; + sourceRegion = default; + + if (target.Width <= 0 || target.Height <= 0) + { + return false; + } + + if ((uint)sourceOffset.X >= (uint)sourceBuffer.Width || (uint)sourceOffset.Y >= (uint)sourceBuffer.Height) + { + return false; + } + + int compositeWidth = Math.Min(target.Width, sourceBuffer.Width - sourceOffset.X); + int compositeHeight = Math.Min(target.Height, sourceBuffer.Height - sourceOffset.Y); + if (compositeWidth <= 0 || compositeHeight <= 0) + { + return false; + } + + sourceRegion = new Buffer2DRegion( + sourceBuffer, + new Rectangle(sourceOffset.X, sourceOffset.Y, compositeWidth, compositeHeight)); + destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); + return true; + } + + public static void CompositeFloatCoverage( + Configuration configuration, + Buffer2DRegion destinationRegion, + Buffer2DRegion sourceRegion, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel + { + using BrushApplicator applicator = brush.CreateApplicator( + configuration, + graphicsOptions, + destinationRegion, + brushBounds); + + int absoluteX = destinationRegion.Rectangle.X; + int absoluteY = destinationRegion.Rectangle.Y; + for (int row = 0; row < sourceRegion.Height; row++) + { + applicator.Apply(sourceRegion.DangerousGetRowSpan(row), absoluteX, absoluteY + row); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs b/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs new file mode 100644 index 000000000..c5c3315a8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Preferred coverage preparation mode for a drawing operation. +/// +internal enum CoveragePreparationMode +{ + /// + /// Backend chooses its default coverage preparation path. + /// + Default = 0, + + /// + /// Backend should use fallback coverage preparation. + /// + Fallback = 1 +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs deleted file mode 100644 index cd2c22ed1..000000000 --- a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Default CPU drawing backend. -/// -/// -/// This backend keeps all CPU-specific scanline handling internal so higher-level processors -/// can remain backend-agnostic. -/// -internal sealed class CpuDrawingBackend : IDrawingBackend -{ - /// - /// Initializes a new instance of the class. - /// - /// Rasterizer used for CPU coverage generation. - private CpuDrawingBackend(IRasterizer primaryRasterizer) - { - Guard.NotNull(primaryRasterizer, nameof(primaryRasterizer)); - this.PrimaryRasterizer = primaryRasterizer; - } - - /// - /// Gets the default backend instance. - /// - public static CpuDrawingBackend Instance { get; } = new(DefaultRasterizer.Instance); - - /// - /// Gets the primary rasterizer used by this backend. - /// - public IRasterizer PrimaryRasterizer { get; } - - /// - /// Creates a backend that uses the given rasterizer as the primary implementation. - /// - /// Primary rasterizer. - /// A backend instance. - public static CpuDrawingBackend Create(IRasterizer rasterizer) - { - Guard.NotNull(rasterizer, nameof(rasterizer)); - return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new CpuDrawingBackend(rasterizer); - } - - /// - public void FillPath( - Configuration configuration, - ImageFrame source, - IPath path, - Brush brush, - in GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - Rectangle brushBounds, - MemoryAllocator allocator) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(source, nameof(source)); - Guard.NotNull(path, nameof(path)); - Guard.NotNull(brush, nameof(brush)); - Guard.NotNull(allocator, nameof(allocator)); - - Rectangle interest = rasterizerOptions.Interest; - if (interest.Equals(Rectangle.Empty)) - { - return; - } - - // Detect the common "opaque solid without blending" case and bypass brush sampling - // for fully covered runs. - TPixel solidBrushColor = default; - bool isSolidBrushWithoutBlending = false; - if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) - { - isSolidBrushWithoutBlending = true; - solidBrushColor = solidBrush.Color.ToPixel(); - } - - int minX = interest.Left; - using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, brushBounds); - FillRasterizationState state = new( - source, - applicator, - minX, - isSolidBrushWithoutBlending, - solidBrushColor); - - this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessRasterizedScanline); - } - - /// - public void RasterizeCoverage( - IPath path, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - Buffer2D destination) - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); - Guard.NotNull(destination, nameof(destination)); - - CoverageRasterizationState state = new(destination); - this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessCoverageScanline); - } - - /// - /// Copies one rasterized coverage row into the destination coverage buffer. - /// - /// Destination row index. - /// Source coverage row. - /// Callback state containing destination storage. - private static void ProcessCoverageScanline(int y, Span scanline, ref CoverageRasterizationState state) - { - Span destination = state.Buffer.DangerousGetRowSpan(y); - scanline.CopyTo(destination); - } - - /// - /// Dispatches rasterized coverage to either the generic brush path or the opaque-solid fast path. - /// - /// The pixel format. - /// Destination row index. - /// Rasterized coverage row. - /// Callback state. - private static void ProcessRasterizedScanline(int y, Span scanline, ref FillRasterizationState state) - where TPixel : unmanaged, IPixel - { - if (state.IsSolidBrushWithoutBlending) - { - ApplyCoverageRunsForOpaqueSolidBrush(state.Source, state.Applicator, scanline, state.MinX, y, state.SolidBrushColor); - } - else - { - ApplyPositiveCoverageRuns(state.Applicator, scanline, state.MinX, y); - } - } - - /// - /// Applies a brush to contiguous positive-coverage runs on a scanline. - /// - /// - /// The rasterizer has already resolved the fill rule (NonZero or EvenOdd) into per-pixel - /// coverage values. This method simply consumes the resulting positive runs. - /// - /// The pixel format. - /// Brush applicator. - /// Coverage values for one row. - /// Absolute X of scanline index 0. - /// Destination row index. - private static void ApplyPositiveCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) - where TPixel : unmanaged, IPixel - { - int i = 0; - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runLength = i - runStart; - if (runLength > 0) - { - // Apply only the positive-coverage run. This avoids invoking brush logic - // for fully transparent gaps. - applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); - } - } - } - - /// - /// Applies coverage using a mixed strategy for opaque solid brushes. - /// - /// - /// Semi-transparent edges still go through brush blending, but fully covered interior runs - /// are written directly with . - /// - /// The pixel format. - /// Destination frame. - /// Brush applicator for non-opaque segments. - /// Coverage values for one row. - /// Absolute X of scanline index 0. - /// Destination row index. - /// Pre-converted solid color for direct writes. - private static void ApplyCoverageRunsForOpaqueSolidBrush( - ImageFrame source, - BrushApplicator applicator, - Span scanline, - int minX, - int y, - TPixel solidBrushColor) - where TPixel : unmanaged, IPixel - { - Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); - int i = 0; - - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runEnd = i; - if (runEnd <= runStart) - { - continue; - } - - // Leading partially-covered segment. - int opaqueStart = runStart; - while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) - { - opaqueStart++; - } - - if (opaqueStart > runStart) - { - int prefixLength = opaqueStart - runStart; - applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); - } - - // Trailing partially-covered segment. - int opaqueEnd = runEnd; - while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) - { - opaqueEnd--; - } - - // Fully covered interior can skip blending entirely. - if (opaqueEnd > opaqueStart) - { - destinationRow[opaqueStart..opaqueEnd].Fill(solidBrushColor); - } - - if (runEnd > opaqueEnd) - { - int suffixLength = runEnd - opaqueEnd; - applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); - } - } - } - - /// - /// Callback state used while writing coverage maps. - /// - private readonly struct CoverageRasterizationState - { - /// - /// Initializes a new instance of the struct. - /// - /// Destination coverage buffer. - public CoverageRasterizationState(Buffer2D buffer) => this.Buffer = buffer; - - /// - /// Gets the destination coverage buffer. - /// - public Buffer2D Buffer { get; } - } - - /// - /// Callback state used while filling into an image frame. - /// - /// The pixel format. - private readonly struct FillRasterizationState - where TPixel : unmanaged, IPixel - { - /// - /// Initializes a new instance of the struct. - /// - /// Destination frame. - /// Brush applicator for blended segments. - /// Absolute X corresponding to scanline index 0. - /// - /// Indicates whether opaque solid fast-path writes are allowed. - /// - /// Pre-converted opaque solid color. - public FillRasterizationState( - ImageFrame source, - BrushApplicator applicator, - int minX, - bool isSolidBrushWithoutBlending, - TPixel solidBrushColor) - { - this.Source = source; - this.Applicator = applicator; - this.MinX = minX; - this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; - this.SolidBrushColor = solidBrushColor; - } - - /// - /// Gets the destination frame. - /// - public ImageFrame Source { get; } - - /// - /// Gets the brush applicator used for blended segments. - /// - public BrushApplicator Applicator { get; } - - /// - /// Gets the absolute X origin of the current scanline. - /// - public int MinX { get; } - - /// - /// Gets a value indicating whether opaque interior runs can be direct-filled. - /// - public bool IsSolidBrushWithoutBlending { get; } - - /// - /// Gets the pre-converted solid color used by the opaque fast path. - /// - public TPixel SolidBrushColor { get; } - } -} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs new file mode 100644 index 000000000..0fdaaae7c --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -0,0 +1,581 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Concurrent; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Default drawing backend. +/// +/// +/// This backend keeps scanline handling internal so higher-level processors +/// can remain backend-agnostic. +/// +internal sealed class DefaultDrawingBackend : IDrawingBackend +{ + private readonly ConcurrentDictionary> preparedCoverage = new(); + private int nextCoverageHandleId; + + /// + /// Initializes a new instance of the class. + /// + /// Rasterizer used for coverage generation. + private DefaultDrawingBackend(IRasterizer primaryRasterizer) + { + Guard.NotNull(primaryRasterizer, nameof(primaryRasterizer)); + this.PrimaryRasterizer = primaryRasterizer; + } + + /// + /// Gets the default backend instance. + /// + public static DefaultDrawingBackend Instance { get; } = new(DefaultRasterizer.Instance); + + /// + /// Gets the primary rasterizer used by this backend. + /// + public IRasterizer PrimaryRasterizer { get; } + + /// + /// Creates a backend that uses the given rasterizer as the primary implementation. + /// + /// Primary rasterizer. + /// A backend instance. + public static DefaultDrawingBackend Create(IRasterizer rasterizer) + { + Guard.NotNull(rasterizer, nameof(rasterizer)); + return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new DefaultDrawingBackend(rasterizer); + } + + /// + public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + } + + /// + public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + } + + /// + public void FillPath( + Configuration configuration, + Buffer2DRegion target, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + where TPixel : unmanaged, IPixel + => FillPath( + configuration, + target, + path, + brush, + graphicsOptions, + rasterizerOptions, + configuration.MemoryAllocator, + this.PrimaryRasterizer); + + /// + public void FillRegion( + Configuration configuration, + Buffer2DRegion target, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle region) + where TPixel : unmanaged, IPixel + => FillRegionCore(configuration, target, brush, graphicsOptions, region); + + /// + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(brush, nameof(brush)); + _ = graphicsOptions; + return true; + } + + /// + public DrawingCoverageHandle PrepareCoverage( + IPath path, + in RasterizerOptions rasterizerOptions, + MemoryAllocator allocator, + CoveragePreparationMode preparationMode) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + _ = preparationMode; + + Size size = rasterizerOptions.Interest.Size; + if (size.Width <= 0 || size.Height <= 0) + { + return default; + } + + Buffer2D destination = allocator.Allocate2D(size, AllocationOptions.Clean); + + CoverageRasterizationState state = new(destination); + this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessCoverageScanline); + + int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); + if (!this.preparedCoverage.TryAdd(handleId, destination)) + { + destination.Dispose(); + throw new InvalidOperationException("Failed to cache prepared coverage."); + } + + return new DrawingCoverageHandle(handleId); + } + + /// + public void CompositeCoverage( + Configuration configuration, + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(brush, nameof(brush)); + + if (!coverageHandle.IsValid) + { + return; + } + + if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out Buffer2D? coverageMap)) + { + throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); + } + + if (!CoverageCompositor.TryGetCompositeRegions( + target, + coverageMap, + sourceOffset, + out Buffer2DRegion destinationRegion, + out Buffer2DRegion sourceRegion)) + { + return; + } + + CoverageCompositor.CompositeFloatCoverage( + configuration, + destinationRegion, + sourceRegion, + brush, + graphicsOptions, + brushBounds); + } + + /// + public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) + { + if (!coverageHandle.IsValid) + { + return; + } + + if (this.preparedCoverage.TryRemove(coverageHandle.Value, out Buffer2D? coverage)) + { + coverage.Dispose(); + } + } + + /// + /// Fills a path into a destination buffer using the configured rasterizer. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination pixel region. + /// The path to rasterize. + /// Brush used to shade covered pixels. + /// Graphics blending/composition options. + /// Rasterizer options. + /// Allocator for temporary data. + /// Rasterizer implementation. + private static void FillPath( + Configuration configuration, + Buffer2DRegion destinationRegion, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + MemoryAllocator allocator, + IRasterizer rasterizer) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(destinationRegion.Buffer, nameof(destinationRegion)); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(allocator, nameof(allocator)); + Guard.NotNull(rasterizer, nameof(rasterizer)); + + Rectangle destinationLocalBounds = new(0, 0, destinationRegion.Width, destinationRegion.Height); + Rectangle interest = Rectangle.Intersect(rasterizerOptions.Interest, destinationLocalBounds); + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + RasterizerOptions clippedRasterizerOptions = rasterizerOptions; + if (!interest.Equals(rasterizerOptions.Interest)) + { + clippedRasterizerOptions = new RasterizerOptions( + interest, + rasterizerOptions.IntersectionRule, + rasterizerOptions.RasterizationMode, + rasterizerOptions.SamplingOrigin); + } + + // Detect the common "opaque solid without blending" case and bypass brush sampling + // for fully covered runs. + TPixel solidBrushColor = default; + bool isSolidBrushWithoutBlending = false; + if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) + { + isSolidBrushWithoutBlending = true; + solidBrushColor = solidBrush.Color.ToPixel(); + } + + int minX = interest.Left; + using BrushApplicator applicator = brush.CreateApplicator( + configuration, + graphicsOptions, + destinationRegion, + path.Bounds); + FillRasterizationState state = new( + destinationRegion, + applicator, + minX, + destinationRegion.Rectangle.X, + destinationRegion.Rectangle.Y, + isSolidBrushWithoutBlending, + solidBrushColor); + + rasterizer.Rasterize(path, clippedRasterizerOptions, allocator, ref state, ProcessRasterizedScanline); + } + + /// + /// Fills a region in destination-local coordinates with the provided brush. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination pixel region. + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + /// Region to fill in destination-local coordinates. + private static void FillRegionCore( + Configuration configuration, + Buffer2DRegion destinationRegion, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle localRegion) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(destinationRegion.Buffer, nameof(destinationRegion)); + Guard.NotNull(brush, nameof(brush)); + + Rectangle destinationLocalBounds = new(0, 0, destinationRegion.Width, destinationRegion.Height); + Rectangle clippedRegion = Rectangle.Intersect(destinationLocalBounds, localRegion); + if (clippedRegion.Equals(Rectangle.Empty)) + { + return; + } + + Buffer2DRegion scopedDestination = destinationRegion.GetSubRegion(clippedRegion); + + if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) + { + TPixel solidBrushColor = solidBrush.Color.ToPixel(); + for (int y = 0; y < scopedDestination.Height; y++) + { + scopedDestination.DangerousGetRowSpan(y).Fill(solidBrushColor); + } + + return; + } + + RectangleF brushRegion = new(clippedRegion.X, clippedRegion.Y, clippedRegion.Width, clippedRegion.Height); + using BrushApplicator applicator = brush.CreateApplicator( + configuration, + graphicsOptions, + scopedDestination, + brushRegion); + using IMemoryOwner amount = configuration.MemoryAllocator.Allocate(scopedDestination.Width); + Span amountSpan = amount.Memory.Span; + amountSpan.Fill(1F); + + int minX = scopedDestination.Rectangle.X; + int minY = scopedDestination.Rectangle.Y; + for (int localY = 0; localY < scopedDestination.Height; localY++) + { + applicator.Apply(amountSpan, minX, minY + localY); + } + } + + /// + /// Dispatches rasterized coverage to either the generic brush path or the opaque-solid fast path. + /// + /// The pixel format. + /// Destination row index. + /// Rasterized coverage row. + /// Callback state. + private static void ProcessRasterizedScanline(int y, Span scanline, ref FillRasterizationState state) + where TPixel : unmanaged, IPixel + { + int absoluteY = y + state.DestinationOffsetY; + int absoluteMinX = state.MinX + state.DestinationOffsetX; + if (state.IsSolidBrushWithoutBlending) + { + ApplyCoverageRunsForOpaqueSolidBrush( + state.DestinationRegion, + state.Applicator, + scanline, + absoluteMinX, + absoluteY, + state.SolidBrushColor); + } + else + { + ApplyPositiveCoverageRuns(state.Applicator, scanline, absoluteMinX, absoluteY); + } + } + + /// + /// Copies one rasterized coverage row into the destination coverage buffer. + /// + /// Destination row index. + /// Source coverage row. + /// Callback state containing destination storage. + private static void ProcessCoverageScanline(int y, Span scanline, ref CoverageRasterizationState state) + { + Span destination = state.Buffer.DangerousGetRowSpan(y); + scanline.CopyTo(destination); + } + + /// + /// Applies a brush to contiguous positive-coverage runs on a scanline. + /// + /// + /// The rasterizer has already resolved the fill rule (NonZero or EvenOdd) into per-pixel + /// coverage values. This method simply consumes the resulting positive runs. + /// + /// The pixel format. + /// Brush applicator. + /// Coverage values for one row. + /// Absolute X of scanline index 0. + /// Destination row index. + private static void ApplyPositiveCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) + where TPixel : unmanaged, IPixel + { + int i = 0; + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runLength = i - runStart; + if (runLength > 0) + { + // Apply only the positive-coverage run. This avoids invoking brush logic + // for fully transparent gaps. + applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); + } + } + } + + /// + /// Applies coverage using a mixed strategy for opaque solid brushes. + /// + /// + /// Semi-transparent edges still go through brush blending, but fully covered interior runs + /// are written directly with . + /// + /// The pixel format. + /// Destination pixel region. + /// Brush applicator for non-opaque segments. + /// Coverage values for one row. + /// Absolute X of scanline index 0. + /// Destination row index. + /// Pre-converted solid color for direct writes. + private static void ApplyCoverageRunsForOpaqueSolidBrush( + Buffer2DRegion destinationRegion, + BrushApplicator applicator, + Span scanline, + int minX, + int y, + TPixel solidBrushColor) + where TPixel : unmanaged, IPixel + { + int localY = y - destinationRegion.Rectangle.Y; + int localX = minX - destinationRegion.Rectangle.X; + Span destinationRow = destinationRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); + int i = 0; + + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runEnd = i; + if (runEnd <= runStart) + { + continue; + } + + // Leading partially-covered segment. + int opaqueStart = runStart; + while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) + { + opaqueStart++; + } + + if (opaqueStart > runStart) + { + int prefixLength = opaqueStart - runStart; + applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); + } + + // Trailing partially-covered segment. + int opaqueEnd = runEnd; + while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) + { + opaqueEnd--; + } + + // Fully covered interior can skip blending entirely. + if (opaqueEnd > opaqueStart) + { + destinationRow[opaqueStart..opaqueEnd].Fill(solidBrushColor); + } + + if (runEnd > opaqueEnd) + { + int suffixLength = runEnd - opaqueEnd; + applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); + } + } + } + + /// + /// Callback state used while writing coverage maps. + /// + private readonly struct CoverageRasterizationState + { + /// + /// Initializes a new instance of the struct. + /// + /// Destination coverage buffer. + public CoverageRasterizationState(Buffer2D buffer) => this.Buffer = buffer; + + /// + /// Gets the destination coverage buffer. + /// + public Buffer2D Buffer { get; } + } + + /// + /// Callback state used while filling into an image frame. + /// + /// The pixel format. + private readonly struct FillRasterizationState + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + /// Destination pixel region. + /// Brush applicator for blended segments. + /// Local X corresponding to scanline index 0. + /// Destination region X offset in target coordinates. + /// Destination region Y offset in target coordinates. + /// + /// Indicates whether opaque solid fast-path writes are allowed. + /// + /// Pre-converted opaque solid color. + public FillRasterizationState( + Buffer2DRegion destinationRegion, + BrushApplicator applicator, + int minX, + int destinationOffsetX, + int destinationOffsetY, + bool isSolidBrushWithoutBlending, + TPixel solidBrushColor) + { + this.DestinationRegion = destinationRegion; + this.Applicator = applicator; + this.MinX = minX; + this.DestinationOffsetX = destinationOffsetX; + this.DestinationOffsetY = destinationOffsetY; + this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; + this.SolidBrushColor = solidBrushColor; + } + + /// + /// Gets the destination pixel region. + /// + public Buffer2DRegion DestinationRegion { get; } + + /// + /// Gets the brush applicator used for blended segments. + /// + public BrushApplicator Applicator { get; } + + /// + /// Gets the local X origin of the current scanline. + /// + public int MinX { get; } + + /// + /// Gets the destination region X offset in target coordinates. + /// + public int DestinationOffsetX { get; } + + /// + /// Gets the destination region Y offset in target coordinates. + /// + public int DestinationOffsetY { get; } + + /// + /// Gets a value indicating whether opaque interior runs can be direct-filled. + /// + public bool IsSolidBrushWithoutBlending { get; } + + /// + /// Gets the pre-converted solid color used by the opaque fast path. + /// + public TPixel SolidBrushColor { get; } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs b/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs new file mode 100644 index 000000000..bff812445 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Opaque handle to backend-prepared coverage data. +/// +internal readonly struct DrawingCoverageHandle : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The backend-specific handle id. + public DrawingCoverageHandle(int value) => this.Value = value; + + /// + /// Gets the raw handle id. + /// + public int Value { get; } + + /// + /// Gets a value indicating whether this handle references prepared coverage. + /// + public bool IsValid => this.Value > 0; + + /// + /// Equality operator. + /// + /// Left value. + /// Right value. + /// if equal. + public static bool operator ==(DrawingCoverageHandle left, DrawingCoverageHandle right) => left.Equals(right); + + /// + /// Inequality operator. + /// + /// Left value. + /// Right value. + /// if not equal. + public static bool operator !=(DrawingCoverageHandle left, DrawingCoverageHandle right) => !(left == right); + + /// + public bool Equals(DrawingCoverageHandle other) => this.Value == other.Value; + + /// + public override bool Equals(object? obj) => obj is DrawingCoverageHandle other && this.Equals(other); + + /// + public override int GetHashCode() => this.Value; +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 92f6f56e4..5e9b5aaed 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -16,47 +16,112 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal interface IDrawingBackend { /// - /// Fills a path into the destination image using the given brush and drawing options. + /// Begins a composition session over a target region. /// /// - /// This operation-level API keeps processors independent from scanline rasterization details, - /// allowing alternate backend implementations (for example GPU backends) to consume brush - /// and path data directly. + /// Backends can use this as an optional batching boundary (for example: keep the destination + /// resident on an accelerator while multiple composite calls are applied). /// /// The pixel format. /// Active processing configuration. - /// Destination image frame. - /// The path to rasterize. + /// Destination target region. + public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel; + + /// + /// Ends a composition session over a target region. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination target region. + public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel; + + /// + /// Fills a path into a destination target region. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination target region. + /// Path in target-local coordinates. /// Brush used to shade covered pixels. /// Graphics blending/composition options. - /// Rasterizer options. - /// Brush bounds used when creating the applicator. - /// Allocator for temporary data. + /// Rasterizer options in target-local coordinates. public void FillPath( Configuration configuration, - ImageFrame source, + Buffer2DRegion target, IPath path, Brush brush, - in GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - Rectangle brushBounds, - MemoryAllocator allocator) + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) where TPixel : unmanaged, IPixel; /// - /// Rasterizes path coverage into a floating-point destination map. + /// Fills a local region in a destination target. /// - /// - /// Coverage values are written in local destination coordinates where (0,0) maps to - /// the top-left of . - /// - /// The path to rasterize. + /// The pixel format. + /// Active processing configuration. + /// Destination target region. + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + /// Region in target-local coordinates. + public void FillRegion( + Configuration configuration, + Buffer2DRegion target, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle region) + where TPixel : unmanaged, IPixel; + + /// + /// Determines whether this backend can composite coverage using the accelerated path + /// for the given brush/options combination. + /// + /// The pixel format. + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + /// when accelerated composition is supported. + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + where TPixel : unmanaged, IPixel; + + /// + /// Prepares coverage for a path and returns a backend-owned handle. + /// + /// The local path to rasterize. /// Rasterizer options. /// Allocator for temporary data. - /// Destination coverage map. - public void RasterizeCoverage( + /// Coverage preparation mode ( or ). + /// An opaque handle to prepared coverage data. + public DrawingCoverageHandle PrepareCoverage( IPath path, in RasterizerOptions rasterizerOptions, MemoryAllocator allocator, - Buffer2D destination); + CoveragePreparationMode preparationMode); + + /// + /// Composites prepared coverage into a destination region using a brush. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination target region. + /// Handle to prepared coverage data. + /// Source offset inside the prepared coverage. + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + /// Brush bounds used when creating the applicator. + public void CompositeCoverage( + Configuration configuration, + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel; + + /// + /// Releases a prepared coverage handle. + /// + /// Handle to release. + public void ReleaseCoverage(DrawingCoverageHandle coverageHandle); } diff --git a/src/ImageSharp.Drawing/Processing/Brush.cs b/src/ImageSharp.Drawing/Processing/Brush.cs index 6b13530f5..6ae966e48 100644 --- a/src/ImageSharp.Drawing/Processing/Brush.cs +++ b/src/ImageSharp.Drawing/Processing/Brush.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -18,7 +20,7 @@ public abstract class Brush : IEquatable /// The pixel type. /// The configuration instance to use when performing operations. /// The graphic options. - /// The source image. + /// The destination pixel region. /// The region the brush will be applied to. /// /// The for this brush. @@ -30,7 +32,7 @@ public abstract class Brush : IEquatable public abstract BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) where TPixel : unmanaged, IPixel; diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs index 99b16023e..54c7b6a5b 100644 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -16,11 +18,14 @@ public abstract class BrushApplicator : IDisposable /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image frame. - protected BrushApplicator(Configuration configuration, GraphicsOptions options, ImageFrame target) + /// The destination pixel region. + protected BrushApplicator( + Configuration configuration, + GraphicsOptions options, + Buffer2DRegion targetRegion) { this.Configuration = configuration; - this.Target = target; + this.TargetRegion = targetRegion; this.Options = options; this.Blender = PixelOperations.Instance.GetPixelBlender(options); } @@ -36,9 +41,9 @@ protected BrushApplicator(Configuration configuration, GraphicsOptions options, internal PixelBlender Blender { get; } /// - /// Gets the target image frame. + /// Gets the destination region. /// - protected ImageFrame Target { get; } + protected Buffer2DRegion TargetRegion { get; } /// /// Gets the graphics options diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs new file mode 100644 index 000000000..1a9d74fa6 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -0,0 +1,485 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// A drawing canvas over a pixel buffer region. +/// +/// The pixel format. +public sealed class DrawingCanvas : IDisposable + where TPixel : unmanaged, IPixel +{ + private readonly Configuration configuration; + private readonly IDrawingBackend backend; + private readonly Buffer2DRegion targetRegion; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// The destination target region. + public DrawingCanvas(Configuration configuration, Buffer2DRegion targetRegion) + : this(configuration, configuration.GetDrawingBackend(), targetRegion) + { + } + + internal DrawingCanvas( + Configuration configuration, + IDrawingBackend backend, + Buffer2DRegion targetRegion) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(targetRegion.Buffer, nameof(targetRegion)); + Guard.NotNull(backend, nameof(backend)); + + this.configuration = configuration; + this.backend = backend; + this.targetRegion = targetRegion; + this.Bounds = new Rectangle(0, 0, targetRegion.Width, targetRegion.Height); + } + + /// + /// Gets the local bounds of this canvas. + /// + public Rectangle Bounds { get; } + + /// + /// Creates a child canvas over a subregion in local coordinates. + /// + /// The child region in local coordinates. + /// A child canvas with local origin at (0,0). + public DrawingCanvas CreateRegion(Rectangle region) + { + this.EnsureNotDisposed(); + + Rectangle clipped = Rectangle.Intersect(this.Bounds, region); + Buffer2DRegion childRegion = this.targetRegion.GetSubRegion(clipped); + return new DrawingCanvas(this.configuration, this.backend, childRegion); + } + + /// + /// Fills the whole canvas using the given brush. + /// + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + public void Fill(Brush brush, GraphicsOptions graphicsOptions) + => this.FillRegion(this.Bounds, brush, graphicsOptions); + + /// + /// Fills a local region using the given brush. + /// + /// Region to fill in local coordinates. + /// Brush used to shade destination pixels. + /// Graphics blending/composition options. + public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOptions) + { + this.EnsureNotDisposed(); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); + + this.backend.FillRegion(this.configuration, this.targetRegion, brush, graphicsOptions, region); + } + + /// + /// Fills a path in local coordinates using the given brush. + /// + /// The path to fill. + /// Brush used to shade covered pixels. + /// Drawing options for fill and rasterization behavior. + public void FillPath(IPath path, Brush brush, DrawingOptions options) + => this.FillPath(path, brush, options, RasterizerSamplingOrigin.PixelBoundary); + + internal void FillPath( + IPath path, + Brush brush, + DrawingOptions options, + RasterizerSamplingOrigin samplingOrigin) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(options, nameof(options)); + + GraphicsOptions graphicsOptions = options.GraphicsOptions; + ShapeOptions shapeOptions = options.ShapeOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + + RectangleF bounds = path.Bounds; + if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + // Keep rasterizer interest aligned with center-sampled scan conversion. + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + shapeOptions.IntersectionRule, + rasterizationMode, + samplingOrigin); + + this.backend.FillPath(this.configuration, this.targetRegion, path, brush, graphicsOptions, rasterizerOptions); + } + + /// + /// Draws a path outline in local coordinates using the given pen. + /// + /// The path to stroke. + /// Pen used to generate the outline fill path. + /// Drawing options for stroke fill and rasterization behavior. + public void DrawPath(IPath path, Pen pen, DrawingOptions options) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(options, nameof(options)); + + IPath outline = pen.GeneratePath(path); + + DrawingOptions effectiveOptions = options; + + // Non-normalized stroke output can self-overlap; non-zero winding preserves stroke semantics. + if (!pen.StrokeOptions.NormalizeOutput && + options.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = options.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + effectiveOptions = new DrawingOptions(options.GraphicsOptions, shapeOptions, options.Transform); + } + + this.FillPath(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); + } + + /// + /// Draws text onto this canvas. + /// + /// The text rendering options. + /// The text to draw. + /// Drawing options defining blending and shape behavior. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public void DrawText( + RichTextOptions textOptions, + string text, + DrawingOptions drawingOptions, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + Guard.NotNull(drawingOptions, nameof(drawingOptions)); + + if (brush is null && pen is null) + { + throw new ArgumentException($"Expected a {nameof(brush)} or {nameof(pen)}. Both were null"); + } + + RichTextOptions configuredOptions = ConfigureTextOptions(textOptions); + using RichTextGlyphRenderer textRenderer = new(configuredOptions, drawingOptions, pen, brush); + TextRenderer renderer = new(textRenderer); + renderer.RenderText(text, configuredOptions); + + this.DrawTextOperations(textRenderer.DrawingOperations, drawingOptions); + } + + private void DrawTextOperations(IEnumerable operations, DrawingOptions drawingOptions) + { + this.EnsureNotDisposed(); + Guard.NotNull(operations, nameof(operations)); + Guard.NotNull(drawingOptions, nameof(drawingOptions)); + + Dictionary coverageCache = []; + this.backend.BeginCompositeSession(this.configuration, this.targetRegion); + try + { + // Operations are layered by render pass (fill, outline, decorations). + foreach (DrawingOperation operation in operations.OrderBy(x => x.RenderPass)) + { + Brush? compositeBrush = GetCompositeBrush(operation); + if (compositeBrush is null) + { + continue; + } + + GraphicsOptions graphicsOptions = + drawingOptions.GraphicsOptions.CloneOrReturnForRules( + operation.PixelAlphaCompositionMode, + operation.PixelColorBlendingMode); + bool useFallbackCoverage = !this.backend.SupportsCoverageComposition(compositeBrush, graphicsOptions); + + if (!this.TryGetCoverage( + operation, + drawingOptions, + useFallbackCoverage, + coverageCache, + out CoverageCacheEntry coverageEntry, + out Point coverageLocation)) + { + continue; + } + + if (!this.TryGetCompositeRegion( + coverageLocation, + coverageEntry.RasterizedSize, + out Buffer2DRegion compositeRegion, + out Point sourceOffset)) + { + continue; + } + + this.backend.CompositeCoverage( + this.configuration, + compositeRegion, + coverageEntry.CoverageHandle, + sourceOffset, + compositeBrush, + graphicsOptions, + this.targetRegion.Rectangle); + } + } + finally + { + foreach ((_, CoverageCacheEntry coverageEntry) in coverageCache) + { + this.backend.ReleaseCoverage(coverageEntry.CoverageHandle); + } + + this.backend.EndCompositeSession(this.configuration, this.targetRegion); + } + } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + } + + private void EnsureNotDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); + + private bool TryGetCoverage( + DrawingOperation operation, + DrawingOptions drawingOptions, + bool useFallbackCoverage, + Dictionary coverageCache, + out CoverageCacheEntry coverageEntry, + out Point coverageLocation) + { + coverageLocation = operation.RenderLocation; + if (!TryCreateCoveragePath(operation, out IPath? coveragePath)) + { + coverageEntry = default; + return false; + } + + Point localOffset = Point.Empty; + if (operation.Kind == DrawingOperationKind.Draw) + { + int strokeHalf = (int)((operation.Pen?.StrokeWidth ?? 0F) / 2F); + coverageLocation = operation.RenderLocation - new Size(strokeHalf, strokeHalf); + + Point coverageMapOrigin = Point.Truncate(coveragePath.Bounds.Location); + localOffset = new Point( + coverageMapOrigin.X - operation.RenderLocation.X, + coverageMapOrigin.Y - operation.RenderLocation.Y); + coveragePath = coveragePath.Translate(-coverageMapOrigin); + } + + OperationCoverageCacheKey cacheKey = CreateOperationCoverageCacheKey(operation, localOffset, useFallbackCoverage); + if (coverageCache.TryGetValue(cacheKey, out coverageEntry)) + { + return true; + } + + Size rasterizedSize = Rectangle.Ceiling(coveragePath.Bounds).Size + new Size(2, 2); + if (rasterizedSize.Width <= 0 || rasterizedSize.Height <= 0) + { + coverageEntry = default; + return false; + } + + RasterizationMode rasterizationMode = drawingOptions.GraphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + RasterizerSamplingOrigin samplingOrigin = operation.Kind == DrawingOperationKind.Draw + ? RasterizerSamplingOrigin.PixelCenter + : RasterizerSamplingOrigin.PixelBoundary; + + RasterizerOptions rasterizerOptions = new( + new Rectangle(0, 0, rasterizedSize.Width, rasterizedSize.Height), + operation.IntersectionRule, + rasterizationMode, + samplingOrigin); + + DrawingCoverageHandle coverageHandle = this.backend.PrepareCoverage( + coveragePath, + rasterizerOptions, + this.configuration.MemoryAllocator, + useFallbackCoverage ? CoveragePreparationMode.Fallback : CoveragePreparationMode.Default); + if (!coverageHandle.IsValid) + { + coverageEntry = default; + return false; + } + + coverageEntry = new CoverageCacheEntry(coverageHandle, rasterizedSize); + coverageCache.Add(cacheKey, coverageEntry); + return true; + } + + private static Brush? GetCompositeBrush(DrawingOperation operation) + { + if (operation.Kind == DrawingOperationKind.Fill) + { + return operation.Brush; + } + + return operation.Pen?.StrokeFill; + } + + private static RichTextOptions ConfigureTextOptions(RichTextOptions options) + { + if (options.Path is not null && options.Origin != Vector2.Zero) + { + // Path-based text uses the path itself as positioning source; fold origin into the path + // to avoid applying both path layout and origin translation. + return new RichTextOptions(options) + { + Origin = Vector2.Zero, + Path = options.Path.Translate(options.Origin) + }; + } + + return options; + } + + private static bool TryCreateCoveragePath( + DrawingOperation operation, + [NotNullWhen(true)] out IPath? coveragePath) + { + if (operation.Kind == DrawingOperationKind.Fill) + { + coveragePath = operation.Path; + return true; + } + + if (operation.Kind == DrawingOperationKind.Draw && operation.Pen is not null) + { + IPath globalPath = operation.Path.Translate(operation.RenderLocation); + coveragePath = operation.Pen.GeneratePath(globalPath); + return true; + } + + coveragePath = null; + return false; + } + + private bool TryGetCompositeRegion( + Point coverageLocation, + Size coverageSize, + out Buffer2DRegion compositeRegion, + out Point sourceOffset) + { + Rectangle destination = new(coverageLocation, coverageSize); + Rectangle clipped = Rectangle.Intersect(this.Bounds, destination); + if (clipped.Equals(Rectangle.Empty)) + { + compositeRegion = default; + sourceOffset = default; + return false; + } + + sourceOffset = new Point(clipped.X - destination.X, clipped.Y - destination.Y); + compositeRegion = this.targetRegion.GetSubRegion(clipped); + return true; + } + + private static OperationCoverageCacheKey CreateOperationCoverageCacheKey( + DrawingOperation operation, + Point localOffset, + bool useFallbackCoverage) + { + int definitionKey = operation.DefinitionKey > 0 + ? operation.DefinitionKey + : CreateFallbackDefinitionKey(operation); + return new OperationCoverageCacheKey(definitionKey, localOffset, useFallbackCoverage); + } + + private static int CreateFallbackDefinitionKey(DrawingOperation operation) + { + HashCode hash = default; + hash.Add(RuntimeHelpers.GetHashCode(operation.Path)); + hash.Add((int)operation.Kind); + hash.Add((int)operation.IntersectionRule); + hash.Add(operation.Brush is null ? 0 : RuntimeHelpers.GetHashCode(operation.Brush)); + hash.Add(operation.Pen is null ? 0 : RuntimeHelpers.GetHashCode(operation.Pen)); + return hash.ToHashCode(); + } + + private readonly struct CoverageCacheEntry + { + public CoverageCacheEntry(DrawingCoverageHandle coverageHandle, Size rasterizedSize) + { + this.CoverageHandle = coverageHandle; + this.RasterizedSize = rasterizedSize; + } + + public DrawingCoverageHandle CoverageHandle { get; } + + public Size RasterizedSize { get; } + } + + private readonly struct OperationCoverageCacheKey : IEquatable + { + private readonly int definitionKey; + private readonly Point localOffset; + private readonly bool useFallbackCoverage; + + public OperationCoverageCacheKey(int definitionKey, Point localOffset, bool useFallbackCoverage) + { + this.definitionKey = definitionKey; + this.localOffset = localOffset; + this.useFallbackCoverage = useFallbackCoverage; + } + + public bool Equals(OperationCoverageCacheKey other) + => this.definitionKey == other.definitionKey + && this.localOffset == other.localOffset + && this.useFallbackCoverage == other.useFallbackCoverage; + + public override bool Equals(object? obj) + => obj is OperationCoverageCacheKey other && this.Equals(other); + + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.definitionKey); + hash.Add(this.localOffset); + hash.Add(this.useFallbackCoverage); + return hash.ToHashCode(); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index fbe4233f0..746f250aa 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -46,12 +47,12 @@ public EllipticGradientBrush( public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new RadialGradientBrushApplicator( configuration, options, - source, + targetRegion, this.center, this.referenceAxisEnd, this.axisRatio, @@ -87,7 +88,7 @@ private sealed class RadialGradientBrushApplicator : GradientBrushApplic /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The destination pixel region. /// Center of the ellipse. /// Point on one angular points of the ellipse. /// @@ -98,13 +99,13 @@ private sealed class RadialGradientBrushApplicator : GradientBrushApplic public RadialGradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame target, + Buffer2DRegion targetRegion, PointF center, PointF referenceAxisEnd, float axisRatio, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target, colorStops, repetitionMode) + : base(configuration, options, targetRegion, colorStops, repetitionMode) { this.center = center; this.referenceAxisEnd = referenceAxisEnd; diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 065125c6a..9aafa9082 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -73,23 +73,23 @@ internal abstract class GradientBrushApplicator : BrushApplicator /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The destination pixel region. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. protected GradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame target, + Buffer2DRegion targetRegion, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target) + : base(configuration, options, targetRegion) { this.colorStops = colorStops; // Ensure the color-stop order is correct. InsertionSort(this.colorStops, (x, y) => x.Ratio.CompareTo(y.Ratio)); this.repetitionMode = repetitionMode; - this.scanlineWidth = target.Width; + this.scanlineWidth = targetRegion.Width; this.allocator = configuration.MemoryAllocator; this.blenderBuffers = new ThreadLocalBlenderBuffers(this.allocator, this.scanlineWidth); } @@ -170,7 +170,9 @@ public override void Apply(Span scanline, int x, int y) } } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); this.Blender.Blend(this.Configuration, destinationRow, destinationRow, overlays, amounts); } diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 5a8062e30..cc4fb6ff3 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -96,16 +96,16 @@ public override bool Equals(Brush? other) public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) { if (this.image is Image specificImage) { - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, false); + return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.region, this.offset, false); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, true); + return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.region, this.offset, true); } /// @@ -140,7 +140,7 @@ private class ImageBrushApplicator : BrushApplicator /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The destination pixel region. /// The image. /// The region of the target image we will be drawing to. /// The region of the source image we will be using to source pixels to draw from. @@ -149,13 +149,13 @@ private class ImageBrushApplicator : BrushApplicator public ImageBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame target, + Buffer2DRegion destinationRegion, Image image, RectangleF targetRegion, RectangleF sourceRegion, Point offset, bool shouldDisposeImage) - : base(configuration, options, target) + : base(configuration, options, destinationRegion) { this.sourceImage = image; this.sourceFrame = image.Frames.RootFrame; @@ -221,7 +221,9 @@ public override void Apply(Span scanline, int x, int y) overlaySpan[i] = sourceRow[sourceX]; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); this.Blender.Blend( this.Configuration, destinationRow, diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index 22692dc0d..a44450329 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; + /// /// Provides a brush that paints linear gradients within an area. /// Supports both classic two-point gradients and three-point (rotated) gradients. @@ -79,12 +81,12 @@ public override int GetHashCode() public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new LinearGradientBrushApplicator( configuration, options, - source, + targetRegion, this.startPoint, this.endPoint, this.rotationPoint, @@ -112,7 +114,7 @@ private sealed class LinearGradientBrushApplicator : GradientBrushApplic /// /// The ImageSharp configuration. /// The graphics options. - /// The target image frame. + /// The destination pixel region. /// The gradient start point. /// The gradient end point. /// The optional rotation point. @@ -121,13 +123,13 @@ private sealed class LinearGradientBrushApplicator : GradientBrushApplic public LinearGradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, PointF p0, PointF p1, PointF? p2, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, source, colorStops, repetitionMode) + : base(configuration, options, targetRegion, colorStops, repetitionMode) { // Determine whether this is a simple linear gradient (2 points) // or a rotated one (3 points). diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index ef3154273..e5a41196c 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -88,12 +89,12 @@ public override int GetHashCode() public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new PathGradientBrushApplicator( configuration, options, - source, + targetRegion, this.edges, this.centerColor, this.hasSpecialCenterColor); @@ -210,18 +211,18 @@ private sealed class PathGradientBrushApplicator : BrushApplicator /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The destination pixel region. /// Edges of the polygon. /// Color at the center of the gradient area to which the other colors converge. /// Whether the center color is different from a smooth gradient between the edges. public PathGradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, IList edges, Color centerColor, bool hasSpecialCenterColor) - : base(configuration, options, source) + : base(configuration, options, targetRegion) { this.edges = edges; Vector2[] points = [.. edges.Select(s => s.Start)]; @@ -232,7 +233,7 @@ public PathGradientBrushApplicator( this.centerPixel = centerColor.ToPixel(); this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length()); this.transparentPixel = Color.Transparent.ToPixel(); - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); + this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, targetRegion.Width); } internal TPixel this[int x, int y] @@ -313,7 +314,9 @@ public override void Apply(Span scanline, int x, int y) } } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); this.Blender.Blend(this.Configuration, destinationRow, destinationRow, overlays, amounts); } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 92bf3db83..ff5a328dc 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -103,12 +104,12 @@ public override int GetHashCode() public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new PatternBrushApplicator( configuration, options, - source, + targetRegion, this.pattern.ToPixelMatrix()); /// @@ -127,17 +128,17 @@ private sealed class PatternBrushApplicator : BrushApplicator /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The destination pixel region. /// The pattern. public PatternBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, in DenseMatrix pattern) - : base(configuration, options, source) + : base(configuration, options, targetRegion) { this.pattern = pattern; - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); + this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, targetRegion.Width); } internal TPixel this[int x, int y] @@ -167,7 +168,9 @@ public override void Apply(Span scanline, int x, int y) overlays[i] = this.pattern[patternY, patternX]; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); this.Blender.Blend( this.Configuration, destinationRow, diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs index 5b3a5cc88..3efbf1617 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; @@ -44,9 +44,7 @@ public DrawPathProcessor(DrawingOptions options, Pen pen, IPath path) public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel { - // Offset drawlines to align drawing outlines to pixel centers. - // The global transform is applied in the FillPathProcessor. - IPath outline = this.Pen.GeneratePath(this.Path.Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); + IPath outline = this.Pen.GeneratePath(this.Path); DrawingOptions effectiveOptions = this.Options; @@ -61,7 +59,11 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio effectiveOptions = new DrawingOptions(this.Options.GraphicsOptions, shapeOptions, this.Options.Transform); } - return new FillPathProcessor(effectiveOptions, this.Pen.StrokeFill, outline) + return new FillPathProcessor( + effectiveOptions, + this.Pen.StrokeFill, + outline, + RasterizerSamplingOrigin.PixelCenter) .CreatePixelSpecificProcessor(configuration, source, sourceRectangle); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 5dfadb974..8b780fe9d 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; @@ -18,10 +19,20 @@ public class FillPathProcessor : IImageProcessor /// The details how to fill the region of interest. /// The logic path to be filled. public FillPathProcessor(DrawingOptions options, Brush brush, IPath path) + : this(options, brush, path, RasterizerSamplingOrigin.PixelBoundary) + { + } + + internal FillPathProcessor( + DrawingOptions options, + Brush brush, + IPath path, + RasterizerSamplingOrigin samplingOrigin) { this.Region = path; this.Brush = brush; this.Options = options; + this.SamplingOrigin = samplingOrigin; } /// @@ -39,13 +50,16 @@ public FillPathProcessor(DrawingOptions options, Brush brush, IPath path) /// public DrawingOptions Options { get; } + internal RasterizerSamplingOrigin SamplingOrigin { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel { IPath shape = this.Region.Transform(this.Options.Transform); - if (shape is RectangularPolygon rectPoly) + if (this.SamplingOrigin == RasterizerSamplingOrigin.PixelBoundary && + shape is RectangularPolygon rectPoly) { RectangleF rectF = new(rectPoly.Location, rectPoly.Size); Rectangle rect = (Rectangle)rectF; @@ -58,7 +72,7 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio } // Clone the definition so we can pass the transformed path. - FillPathProcessor definition = new(this.Options, this.Brush, shape); + FillPathProcessor definition = new(this.Options, this.Brush, shape, this.SamplingOrigin); return new FillPathProcessor(configuration, definition, source, sourceRectangle); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 0788cb6fc..a1ea7aaad 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -1,9 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; @@ -49,8 +46,6 @@ public FillPathProcessor( protected override void OnFrameApply(ImageFrame source) { Configuration configuration = this.Configuration; - ShapeOptions shapeOptions = this.definition.Options.ShapeOptions; - GraphicsOptions graphicsOptions = this.definition.Options.GraphicsOptions; Brush brush = this.definition.Brush; // Align start/end positions. @@ -60,25 +55,10 @@ protected override void OnFrameApply(ImageFrame source) return; // No effect inside image; } - MemoryAllocator allocator = this.Configuration.MemoryAllocator; - IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); - RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - RasterizerOptions rasterizerOptions = new( - interest, - shapeOptions.IntersectionRule, - rasterizationMode, - RasterizerSamplingOrigin.PixelBoundary); - - // The backend owns rasterization/compositing details. Processors only submit - // operation-level data (path, brush, options, bounds). - drawingBackend.FillPath( + using DrawingCanvas canvas = new( configuration, - source, - this.path, - brush, - graphicsOptions, - rasterizerOptions, - this.bounds, - allocator); + new(source.PixelBuffer, source.Bounds)); + + canvas.FillPath(this.path, brush, this.definition.Options, this.definition.SamplingOrigin); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 9e1e3c86f..0fe36075a 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -1,10 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; @@ -32,104 +28,10 @@ protected override void OnFrameApply(ImageFrame source) return; } - Configuration configuration = this.Configuration; - Brush brush = this.definition.Brush; - GraphicsOptions options = this.definition.Options.GraphicsOptions; + using DrawingCanvas canvas = new( + this.Configuration, + new Buffer2DRegion(source.PixelBuffer, interest)); - // If there's no reason for blending, then avoid it. - if (this.IsSolidBrushWithoutBlending(out SolidBrush? solidBrush)) - { - ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration) - .MultiplyMinimumPixelsPerTask(4); - - TPixel colorPixel = solidBrush.Color.ToPixel(); - - FillProcessor.SolidBrushRowIntervalOperation solidOperation = new(interest, source, colorPixel); - ParallelRowIterator.IterateRowIntervals( - interest, - parallelSettings, - in solidOperation); - - return; - } - - using IMemoryOwner amount = configuration.MemoryAllocator.Allocate(interest.Width); - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - options, - source, - this.SourceRectangle); - - amount.Memory.Span.Fill(1F); - - FillProcessor.RowIntervalOperation operation = new(interest, applicator, amount.Memory); - ParallelRowIterator.IterateRowIntervals( - configuration, - interest, - in operation); - } - - private bool IsSolidBrushWithoutBlending([NotNullWhen(true)] out SolidBrush? solidBrush) - { - solidBrush = this.definition.Brush as SolidBrush; - - if (solidBrush is null) - { - return false; - } - - return this.definition.Options.GraphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color); - } - - private readonly struct SolidBrushRowIntervalOperation : IRowIntervalOperation - { - private readonly Rectangle bounds; - private readonly ImageFrame source; - private readonly TPixel color; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SolidBrushRowIntervalOperation(Rectangle bounds, ImageFrame source, TPixel color) - { - this.bounds = bounds; - this.source = source; - this.color = color; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invoke(in RowInterval rows) - { - for (int y = rows.Min; y < rows.Max; y++) - { - this.source.PixelBuffer.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width).Fill(this.color); - } - } - } - - private readonly struct RowIntervalOperation : IRowIntervalOperation - { - private readonly Memory amount; - private readonly Rectangle bounds; - private readonly BrushApplicator applicator; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public RowIntervalOperation(Rectangle bounds, BrushApplicator applicator, Memory amount) - { - this.bounds = bounds; - this.applicator = applicator; - this.amount = amount; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invoke(in RowInterval rows) - { - Span amountSpan = this.amount.Span; - int x = this.bounds.X; - for (int y = rows.Min; y < rows.Max; y++) - { - this.applicator.Apply(amountSpan, x, y); - } - } + canvas.Fill(this.definition.Brush, this.definition.Options.GraphicsOptions); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index a8f60b240..a663a7c1d 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -1,9 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using SixLabors.Fonts.Rendering; -using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; @@ -16,123 +13,24 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; internal class DrawTextProcessor : ImageProcessor where TPixel : unmanaged, IPixel { - private RichTextGlyphRenderer? textRenderer; private readonly DrawTextProcessor definition; public DrawTextProcessor(Configuration configuration, DrawTextProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) => this.definition = definition; - protected override void BeforeImageApply() - { - base.BeforeImageApply(); - - // Do everything at the image level as we are delegating - // the processing down to other processors - RichTextOptions textOptions = ConfigureOptions(this.definition.TextOptions); - - this.textRenderer = new RichTextGlyphRenderer( - textOptions, - this.definition.DrawingOptions, - this.Configuration.MemoryAllocator, - this.Configuration.GetDrawingBackend(), - this.definition.Pen, - this.definition.Brush); - - TextRenderer renderer = new(this.textRenderer); - renderer.RenderText(this.definition.Text, textOptions); - } - - protected override void AfterImageApply() - { - base.AfterImageApply(); - this.textRenderer?.Dispose(); - this.textRenderer = null; - } - /// protected override void OnFrameApply(ImageFrame source) { - void Draw(IEnumerable operations) - { - foreach (DrawingOperation operation in operations) - { - GraphicsOptions graphicsOptions = - this.definition.DrawingOptions.GraphicsOptions.CloneOrReturnForRules( - operation.PixelAlphaCompositionMode, - operation.PixelColorBlendingMode); - - using BrushApplicator app = operation.Brush.CreateApplicator( - this.Configuration, - graphicsOptions, - source, - this.SourceRectangle); - - Buffer2D buffer = operation.Map; - int startY = operation.RenderLocation.Y; - int startX = operation.RenderLocation.X; - int offsetSpan = 0; - - if (startY + buffer.Height < 0) - { - continue; - } - - if (startX + buffer.Width < 0) - { - continue; - } - - if (startX < 0) - { - offsetSpan = -startX; - startX = 0; - } - - if (startX >= source.Width) - { - continue; - } - - int firstRow = 0; - if (startY < 0) - { - firstRow = -startY; - } - - int maxWidth = source.Width - startX; - int maxHeight = source.Height - startY; - int end = Math.Min(operation.Map.Height, maxHeight); - - for (int row = firstRow; row < end; row++) - { - int y = startY + row; - Span span = buffer.DangerousGetRowSpan(row).Slice(offsetSpan, Math.Min(buffer.Width - offsetSpan, maxWidth)); - app.Apply(span, startX, y); - } - } - } - - // Not null, initialized in earlier event. - if (this.textRenderer!.DrawingOperations.Count > 0) - { - Draw(this.textRenderer.DrawingOperations.OrderBy(x => x.RenderPass)); - } - } - - private static RichTextOptions ConfigureOptions(RichTextOptions options) - { - // When a path is specified we should explicitly follow that path - // and not adjust the origin. Any translation should be applied to the path. - if (options.Path is not null && options.Origin != Vector2.Zero) - { - return new RichTextOptions(options) - { - Origin = Vector2.Zero, - Path = options.Path.Translate(options.Origin) - }; - } + using DrawingCanvas canvas = new( + this.Configuration, + new Buffer2DRegion(source.PixelBuffer, source.Bounds)); - return options; + canvas.DrawText( + this.definition.TextOptions, + this.definition.Text, + this.definition.DrawingOptions, + this.definition.Brush, + this.definition.Pen); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs index 1914f4bb5..ef2d657b7 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs @@ -1,21 +1,31 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Memory; - namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +internal enum DrawingOperationKind : byte +{ + Fill = 0, + Draw = 1 +} + internal struct DrawingOperation { - public Buffer2D Map { get; set; } + public int DefinitionKey { get; set; } + + public DrawingOperationKind Kind { get; set; } public IPath Path { get; set; } + public Point RenderLocation { get; set; } + + public IntersectionRule IntersectionRule { get; set; } + public byte RenderPass { get; set; } - public Point RenderLocation { get; set; } + public Brush? Brush { get; set; } - public Brush Brush { get; internal set; } + public Pen? Pen { get; set; } public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index 6436d67ef..ec3b59b3e 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -6,10 +6,7 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -23,8 +20,6 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa private const byte RenderOrderDecoration = 2; private readonly DrawingOptions drawingOptions; - private readonly MemoryAllocator memoryAllocator; - private readonly IDrawingBackend drawingBackend; private readonly Pen? defaultPen; private readonly Brush? defaultBrush; private readonly IPathInternals? path; @@ -46,6 +41,8 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa // - Cache hit ratio above 60% private const float AccuracyMultiple = 8; private readonly Dictionary> glyphCache = []; + private readonly Dictionary operationDefinitionCache = []; + private int nextOperationDefinitionKey = 1; private int cacheReadIndex; private bool rasterizationRequired; @@ -55,15 +52,11 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa public RichTextGlyphRenderer( RichTextOptions textOptions, DrawingOptions drawingOptions, - MemoryAllocator memoryAllocator, - IDrawingBackend drawingBackend, Pen? pen, Brush? brush) : base(drawingOptions.Transform) { this.drawingOptions = drawingOptions; - this.memoryAllocator = memoryAllocator; - this.drawingBackend = drawingBackend; this.defaultPen = pen; this.defaultBrush = brush; this.DrawingOperations = []; @@ -92,12 +85,9 @@ public RichTextGlyphRenderer( /// protected override void BeginText(in FontRectangle bounds) { - foreach (DrawingOperation operation in this.DrawingOperations) - { - operation.Map.Dispose(); - } - this.DrawingOperations.Clear(); + this.operationDefinitionCache.Clear(); + this.nextOperationDefinitionKey = 1; } /// @@ -136,7 +126,11 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple, MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple); - this.currentCacheKey = CacheKey.FromParameters(parameters, new RectangleF(subPixelLocation, subPixelSize)); + this.currentCacheKey = CacheKey.FromParameters( + parameters, + new RectangleF(subPixelLocation, subPixelSize), + this.currentBrush ?? this.defaultBrush, + this.currentPen ?? this.defaultPen); if (this.glyphCache.ContainsKey(this.currentCacheKey)) { // We have already drawn the glyph vectors. @@ -154,6 +148,7 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { this.hasLayer = true; + this.currentFillRule = fillRule; if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush)) { this.currentBrush = brush; @@ -165,6 +160,7 @@ protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? cl protected override void EndLayer() { GlyphRenderData renderData = default; + IPath? fillPath = null; // Fix up the text runs colors. // Only if both brush and pen is null do we fallback to the default value. @@ -190,7 +186,8 @@ protected override void EndLayer() if (renderFill) { - renderData.FillMap = this.Render(path); + renderData.FillPath = path.Translate(-renderLocation); + fillPath = renderData.FillPath; } // Capture the delta between the location and the truncated render location. @@ -233,15 +230,29 @@ protected override void EndLayer() } renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + fillPath = renderData.FillPath; + } } - if (renderData.FillMap != null) + if (fillPath is not null) { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { + DefinitionKey = this.GetOrCreateOperationDefinitionKey( + fillPath, + fillRule, + DrawingOperationKind.Fill, + this.currentBrush, + null), + Kind = DrawingOperationKind.Fill, + Path = fillPath, RenderLocation = renderLocation, - Map = renderData.FillMap, - Brush = this.currentBrush!, + IntersectionRule = fillRule, + Brush = this.currentBrush, RenderPass = RenderOrderFill, PixelAlphaCompositionMode = this.currentCompositionMode, PixelColorBlendingMode = this.currentBlendingMode @@ -359,11 +370,22 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star } // Render the path here. Decorations are un-cached. + Point renderLocation = ClampToPixel(outline.Bounds.Location); + IPath decorationPath = outline.Translate(-renderLocation); + Brush decorationBrush = pen.StrokeFill; this.DrawingOperations.Add(new DrawingOperation { - Brush = pen.StrokeFill, - RenderLocation = ClampToPixel(outline.Bounds.Location), - Map = this.Render(outline), + DefinitionKey = this.GetOrCreateOperationDefinitionKey( + decorationPath, + IntersectionRule.NonZero, + DrawingOperationKind.Fill, + decorationBrush, + null), + Kind = DrawingOperationKind.Fill, + Path = decorationPath, + RenderLocation = renderLocation, + IntersectionRule = IntersectionRule.NonZero, + Brush = decorationBrush, RenderPass = RenderOrderDecoration }); } @@ -378,6 +400,7 @@ protected override void EndGlyph() } GlyphRenderData renderData = default; + IPath? glyphPath = null; // Fix up the text runs colors. // Only if both brush and pen is null do we fallback to the default value. @@ -412,21 +435,17 @@ protected override void EndGlyph() return; } - if (renderFill) + IPath localPath = path.Translate(-renderLocation); + if (renderFill || renderOutline) { - renderData.FillMap = this.Render(path); + renderData.FillPath = localPath; + glyphPath = renderData.FillPath; } // Capture the delta between the location and the truncated render location. // We can use this to offset the render location on the next instance of this glyph. renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); - if (renderOutline) - { - path = this.currentPen!.GeneratePath(path); - renderData.OutlineMap = this.Render(path); - } - if (!this.noCache) { this.UpdateCache(renderData); @@ -463,29 +482,56 @@ protected override void EndGlyph() } renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } + + if (renderOutline && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } } - if (renderData.FillMap != null) + if (renderFill && glyphPath is not null) { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { + DefinitionKey = this.GetOrCreateOperationDefinitionKey( + glyphPath, + fillRule, + DrawingOperationKind.Fill, + this.currentBrush, + null), + Kind = DrawingOperationKind.Fill, + Path = glyphPath, RenderLocation = renderLocation, - Map = renderData.FillMap, - Brush = this.currentBrush!, + IntersectionRule = fillRule, + Brush = this.currentBrush, RenderPass = RenderOrderFill, PixelAlphaCompositionMode = this.currentCompositionMode, PixelColorBlendingMode = this.currentBlendingMode }); } - if (renderData.OutlineMap != null) + if (renderOutline && glyphPath is not null) { - int offset = (int)((this.currentPen?.StrokeWidth ?? 0) / 2); + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { - RenderLocation = renderLocation - new Size(offset, offset), - Map = renderData.OutlineMap, - Brush = this.currentPen?.StrokeFill ?? this.currentBrush!, + DefinitionKey = this.GetOrCreateOperationDefinitionKey( + glyphPath, + outlineRule, + DrawingOperationKind.Draw, + null, + this.currentPen), + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = this.currentPen, RenderPass = RenderOrderOutline, PixelAlphaCompositionMode = this.currentCompositionMode, PixelColorBlendingMode = this.currentBlendingMode @@ -503,6 +549,24 @@ private void UpdateCache(GlyphRenderData renderData) this.glyphCache[this.currentCacheKey].Add(renderData); } + private int GetOrCreateOperationDefinitionKey( + IPath path, + IntersectionRule intersectionRule, + DrawingOperationKind kind, + Brush? brush, + Pen? pen) + { + OperationDefinitionCacheKey cacheKey = new(path, intersectionRule, kind, brush, pen); + if (this.operationDefinitionCache.TryGetValue(cacheKey, out int existing)) + { + return existing; + } + + int next = this.nextOperationDefinitionKey++; + this.operationDefinitionCache.Add(cacheKey, next); + return next; + } + public void Dispose() => this.Dispose(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -530,67 +594,14 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds) return Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); } - /// - /// Rasterizes a glyph path to a local coverage map. - /// - /// The glyph path in destination coordinates. - /// A coverage buffer used by later text draw operations. - private Buffer2D Render(IPath path) - { - // We need to offset the path now by the difference between the clamped location and the - // path location. - IPath offsetPath = path.Translate(-ClampToPixel(path.Bounds.Location)); - Size size = Rectangle.Ceiling(offsetPath.Bounds).Size; - - // Pad to prevent edge clipping. - size += new Size(2, 2); - - RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; - GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions; - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - // Take the path inside the path builder, scan thing and generate a Buffer2D representing the glyph. - Buffer2D buffer = this.memoryAllocator.Allocate2D(size.Width, size.Height, AllocationOptions.Clean); - RasterizerOptions rasterizerOptions = new( - new Rectangle(0, 0, size.Width, size.Height), - TextUtilities.MapFillRule(this.currentFillRule), - rasterizationMode, - samplingOrigin); - - // Request coverage generation from the configured backend. CPU backends will produce - // this via scanlines; future GPU backends can supply equivalent coverage by other means. - this.drawingBackend.RasterizeCoverage( - offsetPath, - rasterizerOptions, - this.memoryAllocator, - buffer); - - return buffer; - } - private void Dispose(bool disposing) { if (!this.isDisposed) { if (disposing) { - foreach (KeyValuePair> kv in this.glyphCache) - { - foreach (GlyphRenderData data in kv.Value) - { - data.Dispose(); - } - } - this.glyphCache.Clear(); - - foreach (DrawingOperation operation in this.DrawingOperations) - { - operation.Map.Dispose(); - } - + this.operationDefinitionCache.Clear(); this.DrawingOperations.Clear(); } @@ -598,21 +609,57 @@ private void Dispose(bool disposing) } } - private struct GlyphRenderData : IDisposable + private readonly struct OperationDefinitionCacheKey : IEquatable { - public Vector2 LocationDelta; + private readonly IPath path; + private readonly IntersectionRule intersectionRule; + private readonly DrawingOperationKind kind; + private readonly Brush? brush; + private readonly Pen? pen; + + public OperationDefinitionCacheKey( + IPath path, + IntersectionRule intersectionRule, + DrawingOperationKind kind, + Brush? brush, + Pen? pen) + { + this.path = path; + this.intersectionRule = intersectionRule; + this.kind = kind; + this.brush = brush; + this.pen = pen; + } - public Buffer2D FillMap; + public bool Equals(OperationDefinitionCacheKey other) + => ReferenceEquals(this.path, other.path) + && this.intersectionRule == other.intersectionRule + && this.kind == other.kind + && ReferenceEquals(this.brush, other.brush) + && ReferenceEquals(this.pen, other.pen); - public Buffer2D OutlineMap; + public override bool Equals(object? obj) + => obj is OperationDefinitionCacheKey other && this.Equals(other); - public readonly void Dispose() + public override int GetHashCode() { - this.FillMap?.Dispose(); - this.OutlineMap?.Dispose(); + HashCode hash = default; + hash.Add(RuntimeHelpers.GetHashCode(this.path)); + hash.Add((int)this.intersectionRule); + hash.Add((int)this.kind); + hash.Add(this.brush is null ? 0 : RuntimeHelpers.GetHashCode(this.brush)); + hash.Add(this.pen is null ? 0 : RuntimeHelpers.GetHashCode(this.pen)); + return hash.ToHashCode(); } } + private struct GlyphRenderData + { + public Vector2 LocationDelta; + + public IPath? FillPath; + } + private readonly struct CacheKey : IEquatable { public string Font { get; init; } @@ -641,11 +688,19 @@ public readonly void Dispose() public RectangleF Bounds { get; init; } + public Brush? BrushReference { get; init; } + + public Pen? PenReference { get; init; } + public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); - public static CacheKey FromParameters(in GlyphRendererParameters parameters, RectangleF bounds) + public static CacheKey FromParameters( + in GlyphRendererParameters parameters, + RectangleF bounds, + Brush? brushReference, + Pen? penReference) => new() { // Do not include the grapheme index as that will @@ -661,7 +716,9 @@ public static CacheKey FromParameters(in GlyphRendererParameters parameters, Rec LayoutMode = parameters.LayoutMode, TextAttributes = parameters.TextRun.TextAttributes, TextDecorations = parameters.TextRun.TextDecorations, - Bounds = bounds + Bounds = bounds, + BrushReference = brushReference, + PenReference = penReference }; public override bool Equals(object? obj) @@ -680,7 +737,9 @@ public bool Equals(CacheKey other) this.LayoutMode == other.LayoutMode && this.TextAttributes == other.TextAttributes && this.TextDecorations == other.TextDecorations && - this.Bounds.Equals(other.Bounds); + this.Bounds.Equals(other.Bounds) && + ReferenceEquals(this.BrushReference, other.BrushReference) && + ReferenceEquals(this.PenReference, other.PenReference); public override int GetHashCode() { @@ -698,6 +757,8 @@ public override int GetHashCode() hash.Add(this.TextAttributes); hash.Add(this.TextDecorations); hash.Add(this.Bounds); + hash.Add(this.BrushReference is null ? 0 : RuntimeHelpers.GetHashCode(this.BrushReference)); + hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference)); return hash.ToHashCode(); } } diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 6e13391bb..1fcae55f4 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; + /// /// A radial gradient brush defined by either one circle or two circles. /// When one circle is provided, the gradient parameter is the distance from the center divided by the radius. @@ -83,12 +85,12 @@ public override int GetHashCode() public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new RadialGradientBrushApplicator( configuration, options, - source, + targetRegion, this.center0, this.radius0, this.center1, @@ -125,7 +127,7 @@ private sealed class RadialGradientBrushApplicator : GradientBrushApplic /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The destination pixel region. /// Center of the starting circle. /// Radius of the starting circle. /// Center of the ending circle, or null to use single-circle form. @@ -135,14 +137,14 @@ private sealed class RadialGradientBrushApplicator : GradientBrushApplic public RadialGradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame target, + Buffer2DRegion targetRegion, PointF center0, float radius0, PointF? center1, float? radius1, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target, colorStops, repetitionMode) + : base(configuration, options, targetRegion, colorStops, repetitionMode) { this.c0x = center0.X; this.c0y = center0.Y; diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs index db2361cfc..510101e10 100644 --- a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -22,9 +22,9 @@ internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingC Guard.NotNull(backend, nameof(backend)); context.Properties[typeof(IDrawingBackend)] = backend; - if (backend is CpuDrawingBackend cpuBackend) + if (backend is DefaultDrawingBackend defaultBackend) { - context.Properties[typeof(IRasterizer)] = cpuBackend.PrimaryRasterizer; + context.Properties[typeof(IRasterizer)] = defaultBackend.PrimaryRasterizer; } return context; @@ -40,9 +40,9 @@ internal static void SetDrawingBackend(this Configuration configuration, IDrawin Guard.NotNull(backend, nameof(backend)); configuration.Properties[typeof(IDrawingBackend)] = backend; - if (backend is CpuDrawingBackend cpuBackend) + if (backend is DefaultDrawingBackend defaultBackend) { - configuration.Properties[typeof(IRasterizer)] = cpuBackend.PrimaryRasterizer; + configuration.Properties[typeof(IRasterizer)] = defaultBackend.PrimaryRasterizer; } } @@ -62,7 +62,7 @@ internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext c if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && rasterizer is IRasterizer configuredRasterizer) { - return CpuDrawingBackend.Create(configuredRasterizer); + return DefaultDrawingBackend.Create(configuredRasterizer); } return context.Configuration.GetDrawingBackend(); @@ -84,12 +84,12 @@ internal static IDrawingBackend GetDrawingBackend(this Configuration configurati if (configuration.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && rasterizer is IRasterizer configuredRasterizer) { - IDrawingBackend rasterizerBackend = CpuDrawingBackend.Create(configuredRasterizer); + IDrawingBackend rasterizerBackend = DefaultDrawingBackend.Create(configuredRasterizer); configuration.Properties[typeof(IDrawingBackend)] = rasterizerBackend; return rasterizerBackend; } - IDrawingBackend defaultBackend = CpuDrawingBackend.Instance; + IDrawingBackend defaultBackend = DefaultDrawingBackend.Instance; configuration.Properties[typeof(IDrawingBackend)] = defaultBackend; return defaultBackend; } @@ -104,7 +104,7 @@ internal static IImageProcessingContext SetRasterizer(this IImageProcessingConte { Guard.NotNull(rasterizer, nameof(rasterizer)); context.Properties[typeof(IRasterizer)] = rasterizer; - context.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Create(rasterizer); + context.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Create(rasterizer); return context; } @@ -117,7 +117,7 @@ internal static void SetRasterizer(this Configuration configuration, IRasterizer { Guard.NotNull(rasterizer, nameof(rasterizer)); configuration.Properties[typeof(IRasterizer)] = rasterizer; - configuration.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Create(rasterizer); + configuration.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Create(rasterizer); } /// @@ -134,9 +134,9 @@ internal static IRasterizer GetRasterizer(this IImageProcessingContext context) } if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is CpuDrawingBackend cpuBackend) + backend is DefaultDrawingBackend defaultBackend) { - return cpuBackend.PrimaryRasterizer; + return defaultBackend.PrimaryRasterizer; } // Do not cache config fallback in the context so changes on configuration reflow. @@ -157,14 +157,14 @@ internal static IRasterizer GetRasterizer(this Configuration configuration) } if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is CpuDrawingBackend cpuBackend) + backend is DefaultDrawingBackend defaultBackend) { - return cpuBackend.PrimaryRasterizer; + return defaultBackend.PrimaryRasterizer; } IRasterizer defaultRasterizer = DefaultRasterizer.Instance; configuration.Properties[typeof(IRasterizer)] = defaultRasterizer; - configuration.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Instance; + configuration.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Instance; return defaultRasterizer; } } diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index c9369761e..1592c6448 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -43,11 +44,11 @@ public RecolorBrush(Color sourceColor, Color targetColor, float threshold) public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new RecolorBrushApplicator( configuration, options, - source, + targetRegion, this.SourceColor.ToPixel(), this.TargetColor.ToPixel(), this.Threshold); @@ -87,18 +88,18 @@ private class RecolorBrushApplicator : BrushApplicator /// /// The configuration instance to use when performing operations. /// The options - /// The source image. + /// The destination pixel region. /// Color of the source. /// Color of the target. /// The threshold . public RecolorBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, TPixel sourceColor, TPixel targetColor, float threshold) - : base(configuration, options, source) + : base(configuration, options, targetRegion) { this.sourceColor = sourceColor.ToScaledVector4(); this.targetColorPixel = targetColor; @@ -108,7 +109,7 @@ public RecolorBrushApplicator( TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue)); TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue)); this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); + this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, targetRegion.Width); } internal TPixel this[int x, int y] @@ -116,7 +117,9 @@ public RecolorBrushApplicator( get { // Offset the requested pixel by the value in the rectangle (the shapes position) - TPixel result = this.Target[x, y]; + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + TPixel result = this.TargetRegion.DangerousGetRowSpan(localY)[localX]; Vector4 background = result.ToVector4(); float distance = Vector4.DistanceSquared(background, this.sourceColor); if (distance <= this.threshold) @@ -135,28 +138,38 @@ public RecolorBrushApplicator( /// public override void Apply(Span scanline, int x, int y) { - if (x < 0 || y < 0 || x >= this.Target.Width || y >= this.Target.Height) + Rectangle targetBounds = this.TargetRegion.Rectangle; + if (y < targetBounds.Y || y >= targetBounds.Bottom) { return; } - // Limit the scanline to the bounds of the image relative to x. - scanline = scanline[..Math.Min(this.Target.Width - x, scanline.Length)]; - Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; - Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; + int startX = Math.Max(x, targetBounds.X); + int endX = Math.Min(x + scanline.Length, targetBounds.Right); + if (startX >= endX) + { + return; + } + + int length = endX - startX; + Span clippedScanline = scanline.Slice(startX - x, length); + Span amounts = this.blenderBuffers.AmountSpan[..length]; + Span overlays = this.blenderBuffers.OverlaySpan[..length]; - for (int i = 0; i < scanline.Length; i++) + for (int i = 0; i < clippedScanline.Length; i++) { - amounts[i] = scanline[i] * this.Options.BlendPercentage; + amounts[i] = clippedScanline[i] * this.Options.BlendPercentage; - int offsetX = x + i; + int offsetX = startX + i; // No doubt this one can be optimized further but I can't imagine its // actually being used and can probably be removed/internalized for now overlays[i] = this[offsetX, y]; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); + int localY = y - targetBounds.Y; + int localX = startX - targetBounds.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX, length); this.Blender.Blend( this.Configuration, destinationRow, diff --git a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs index 268592a69..383a06772 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs @@ -25,7 +25,25 @@ public RichTextOptions(Font font) /// The options whose properties are copied into this instance. public RichTextOptions(RichTextOptions options) : base(options) - => this.Path = options.Path; + { + this.Path = options.Path; + List runs = new(options.TextRuns.Count); + foreach (RichTextRun run in options.TextRuns) + { + runs.Add(new RichTextRun() + { + Brush = run.Brush, + Pen = run.Pen, + StrikeoutPen = run.StrikeoutPen, + UnderlinePen = run.UnderlinePen, + OverlinePen = run.OverlinePen, + Start = run.Start, + End = run.End + }); + } + + this.TextRuns = runs; + } /// /// Gets or sets an optional collection of text runs to apply to the body of text. diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index 41c3e0717..9d3a0407f 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -3,6 +3,7 @@ using System.Buffers; using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -26,8 +27,8 @@ public sealed class SolidBrush : Brush public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, - RectangleF region) => new SolidBrushApplicator(configuration, options, source, this.Color.ToPixel()); + Buffer2DRegion targetRegion, + RectangleF region) => new SolidBrushApplicator(configuration, options, targetRegion, this.Color.ToPixel()); /// public override bool Equals(Brush? other) @@ -59,26 +60,28 @@ private sealed class SolidBrushApplicator : BrushApplicator /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The destination pixel region. /// The color. public SolidBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, TPixel color) - : base(configuration, options, source) + : base(configuration, options, targetRegion) { - this.colors = configuration.MemoryAllocator.Allocate(source.Width); + this.colors = configuration.MemoryAllocator.Allocate(targetRegion.Width); this.colors.Memory.Span.Fill(color); // The threadlocal value is lazily invoked so there is no need to optionally create the type. - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width, true); + this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, targetRegion.Width, true); } /// public override void Apply(Span scanline, int x, int y) { - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x); + int localY = y - this.TargetRegion.Rectangle.Y; + int localX = x - this.TargetRegion.Rectangle.X; + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX); // Constrain the spans to each other if (destinationRow.Length > scanline.Length) diff --git a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs index 5aed66787..2ef27c2f8 100644 --- a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; + /// /// Provides an implementation of a brush for painting sweep (conic) gradients within areas. /// Angles increase clockwise (y-down coordinate system) with 0° pointing to the +X direction. @@ -62,12 +64,12 @@ public override int GetHashCode() public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, RectangleF region) => new SweepGradientBrushApplicator( configuration, options, - source, + targetRegion, this.center, this.startAngleDegrees, this.endAngleDegrees, @@ -98,7 +100,7 @@ private sealed class SweepGradientBrushApplicator : GradientBrushApplica /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The destination pixel region. /// The center of the sweep gradient. /// The start angle in degrees (clockwise). /// The end angle in degrees (clockwise). @@ -107,13 +109,13 @@ private sealed class SweepGradientBrushApplicator : GradientBrushApplica public SweepGradientBrushApplicator( Configuration configuration, GraphicsOptions options, - ImageFrame source, + Buffer2DRegion targetRegion, PointF center, float startAngleDegrees, float endAngleDegrees, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, source, colorStops, repetitionMode) + : base(configuration, options, targetRegion, colorStops, repetitionMode) { this.cx = center.X; this.cy = center.Y; diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 150b2612c..480d7a636 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -117,13 +117,22 @@ private static void RasterizeCore( } int coverStrideInt = (int)coverStride; - float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + bool samplePixelCenter = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; // Create tessellated rings once. Both sequential and parallel paths consume this single // canonical representation so path flattening/orientation work is never repeated. using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); - int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeDataOwner.Memory.Span); + int edgeCount = BuildEdgeTable( + multipolygon, + interest.Left, + interest.Top, + height, + samplingOffsetX, + samplingOffsetY, + edgeDataOwner.Memory.Span); if (edgeCount <= 0) { return; @@ -623,6 +632,7 @@ private static void CaptureTileScanline(int y, Span scanline, ref TileCap /// Interest top in absolute coordinates. /// Interest height in pixels. /// Horizontal sampling offset. + /// Vertical sampling offset. /// Destination span for edge records. /// Number of valid edge records written. private static int BuildEdgeTable( @@ -631,6 +641,7 @@ private static int BuildEdgeTable( int minY, int height, float samplingOffsetX, + float samplingOffsetY, Span destination) { int count = 0; @@ -643,9 +654,9 @@ private static int BuildEdgeTable( PointF p1 = vertices[i + 1]; float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = p0.Y - minY; + float y0 = (p0.Y - minY) + samplingOffsetY; float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = p1.Y - minY; + float y1 = (p1.Y - minY) + samplingOffsetY; if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) { @@ -1008,7 +1019,13 @@ public Context( /// Absolute left coordinate of the current scanner window. /// Absolute top coordinate of the current scanner window. /// Horizontal sample origin offset. - public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX, int minY, float samplingOffsetX) + /// Vertical sample origin offset. + public void RasterizeMultipolygon( + TessellatedMultipolygon multipolygon, + int minX, + int minY, + float samplingOffsetX, + float samplingOffsetY) { foreach (TessellatedMultipolygon.Ring ring in multipolygon) { @@ -1019,9 +1036,9 @@ public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX PointF p1 = vertices[i + 1]; float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = p0.Y - minY; + float y0 = (p0.Y - minY) + samplingOffsetY; float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = p1.Y - minY; + float y1 = (p1.Y - minY) + samplingOffsetY; if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) { diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD index e4fb24455..8ca955856 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD @@ -47,7 +47,7 @@ Choose execution mode: - Geometry is transformed to scanner-local coordinates: - `xLocal = (x - interest.Left) + samplingOffsetX` - - `yLocal = y - interest.Top` (global local-space edge table) + - `yLocal = (y - interest.Top) + samplingOffsetY` (global local-space edge table) - Per tile/band pass uses `yLocal - currentBandTop` - Scanner math uses signed 24.8 fixed point: - `FixedShift = 8` @@ -184,7 +184,7 @@ rasterization time. - `RasterizerOptions.RasterizationMode` controls whether scanner output is: - `Antialiased`: continuous coverage in `[0, 1]` - `Aliased`: binary coverage (`0` or `1`), thresholded in the scanner -- `RasterizerSamplingOrigin` still affects X alignment (`PixelBoundary` vs `PixelCenter`). +- `RasterizerSamplingOrigin` affects both X and Y sample alignment (`PixelBoundary` vs `PixelCenter`). ## Data Flow Diagram (Row-Level) diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index a7b7f0564..c082ef741 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -32,7 +32,7 @@ - + @@ -51,6 +51,7 @@ + diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs new file mode 100644 index 000000000..442658d17 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -0,0 +1,269 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Concurrent; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable +{ + private readonly ConcurrentDictionary preparedCoverage = new(); + private int nextCoverageHandleId; + private bool isDisposed; + + public int PrepareCoverageCallCount { get; private set; } + + public int CompositeCoverageCallCount { get; private set; } + + public int ReleaseCoverageCallCount { get; private set; } + + public int LiveCoverageCount => this.preparedCoverage.Count; + + public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + } + + public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + } + + public void FillPath( + Configuration configuration, + Buffer2DRegion target, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + where TPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.FillPath( + configuration, + target, + path, + brush, + graphicsOptions, + rasterizerOptions); + + public void FillRegion( + Configuration configuration, + Buffer2DRegion target, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle region) + where TPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.FillRegion( + configuration, + target, + brush, + graphicsOptions, + region); + + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + where TPixel : unmanaged, IPixel + { + ArgumentNullException.ThrowIfNull(brush); + _ = graphicsOptions; + return true; + } + + public DrawingCoverageHandle PrepareCoverage( + IPath path, + in RasterizerOptions rasterizerOptions, + MemoryAllocator allocator, + CoveragePreparationMode preparationMode) + { + ArgumentNullException.ThrowIfNull(path); + + ArgumentNullException.ThrowIfNull(allocator); + _ = preparationMode; + + this.PrepareCoverageCallCount++; + + Size size = rasterizerOptions.Interest.Size; + if (size.Width <= 0 || size.Height <= 0) + { + return default; + } + + SKImageInfo imageInfo = new(size.Width, size.Height, SKColorType.Alpha8, SKAlphaType.Unpremul); + SKBitmap bitmap = new(imageInfo); + using SKCanvas canvas = new(bitmap); + canvas.Clear(SKColors.Transparent); + + if (rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + canvas.Translate(0.5F, 0.5F); + } + + using SKPath skPath = CreateSkPath(path, rasterizerOptions.Interest.Location, rasterizerOptions.IntersectionRule); + using SKPaint paint = new() + { + Color = SKColors.White, + Style = SKPaintStyle.Fill, + IsAntialias = rasterizerOptions.RasterizationMode == RasterizationMode.Antialiased + }; + + canvas.DrawPath(skPath, paint); + + int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); + if (!this.preparedCoverage.TryAdd(handleId, bitmap)) + { + bitmap.Dispose(); + throw new InvalidOperationException("Failed to cache prepared coverage."); + } + + return new DrawingCoverageHandle(handleId); + } + + public void CompositeCoverage( + Configuration configuration, + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel + { + ArgumentNullException.ThrowIfNull(configuration); + + if (target.Buffer is null) + { + throw new ArgumentNullException(nameof(target)); + } + + ArgumentNullException.ThrowIfNull(brush); + + this.CompositeCoverageCallCount++; + + if (!coverageHandle.IsValid) + { + return; + } + + if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out SKBitmap bitmap)) + { + throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); + } + + if (bitmap.ColorType != SKColorType.Alpha8) + { + throw new InvalidOperationException($"Prepared coverage '{coverageHandle.Value}' is not Alpha8."); + } + + if ((uint)sourceOffset.X >= (uint)bitmap.Width || (uint)sourceOffset.Y >= (uint)bitmap.Height) + { + return; + } + + int compositeWidth = Math.Min(target.Width, bitmap.Width - sourceOffset.X); + int compositeHeight = Math.Min(target.Height, bitmap.Height - sourceOffset.Y); + if (compositeWidth <= 0 || compositeHeight <= 0) + { + return; + } + + using BrushApplicator applicator = brush.CreateApplicator( + configuration, + graphicsOptions, + target, + brushBounds); + + ReadOnlySpan source = bitmap.GetPixelSpan(); + int rowBytes = bitmap.RowBytes; + int absoluteX = target.Rectangle.X; + int absoluteY = target.Rectangle.Y; + + float[] rented = ArrayPool.Shared.Rent(compositeWidth); + try + { + Span coverage = rented.AsSpan(0, compositeWidth); + for (int row = 0; row < compositeHeight; row++) + { + int srcRow = (sourceOffset.Y + row) * rowBytes; + int srcOffset = srcRow + sourceOffset.X; + for (int x = 0; x < compositeWidth; x++) + { + coverage[x] = source[srcOffset + x] / 255F; + } + + applicator.Apply(coverage, absoluteX, absoluteY + row); + } + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) + { + this.ReleaseCoverageCallCount++; + + if (!coverageHandle.IsValid) + { + return; + } + + if (this.preparedCoverage.TryRemove(coverageHandle.Value, out SKBitmap bitmap)) + { + bitmap.Dispose(); + } + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + foreach (KeyValuePair kv in this.preparedCoverage) + { + kv.Value.Dispose(); + } + + this.preparedCoverage.Clear(); + this.isDisposed = true; + } + + private static SKPath CreateSkPath(IPath path, Point interestLocation, IntersectionRule intersectionRule) + { + SKPath skPath = new() + { + FillType = intersectionRule == IntersectionRule.EvenOdd + ? SKPathFillType.EvenOdd + : SKPathFillType.Winding + }; + + float offsetX = -interestLocation.X; + float offsetY = -interestLocation.Y; + + foreach (ISimplePath simplePath in path.Flatten()) + { + ReadOnlySpan points = simplePath.Points.Span; + if (points.Length == 0) + { + continue; + } + + SKPoint[] skPoints = new SKPoint[points.Length]; + for (int i = 0; i < points.Length; i++) + { + skPoints[i] = new SKPoint(points[i].X + offsetX, points[i].Y + offsetY); + } + + skPath.AddPoly(skPoints, true); + } + + return skPath; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs new file mode 100644 index 000000000..dc86d0b20 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +[GroupOutput("Drawing")] +public class SkiaCoverageDrawingBackendTests +{ + [Theory] + [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] + public void DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(18, 28) + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = "Sphinx of black quartz, judge my vow\n0123456789"; + Brush brush = Brushes.Solid(Color.Black); + Pen pen = Pens.Solid(Color.OrangeRed, 2F); + + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + defaultImage.DebugSave( + provider, + "DefaultBackend_DrawText", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image skiaBackendImage = provider.GetImage(); + using SkiaCoverageDrawingBackend backend = new(); + skiaBackendImage.Configuration.SetDrawingBackend(backend); + skiaBackendImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + + skiaBackendImage.DebugSave( + provider, + "SkiaBackend_DrawText", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.True(backend.PrepareCoverageCallCount > 0); + Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + + ImageComparer comparer = ImageComparer.TolerantPercentage(4F); + comparer.VerifySimilarity(defaultImage, skiaBackendImage); + } + + [Theory] + [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + + using Image image = provider.GetImage(); + using SkiaCoverageDrawingBackend backend = new(); + image.Configuration.SetDrawingBackend(backend); + + image.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); + + image.DebugSave( + provider, + "SkiaBackend_RepeatedGlyphs", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.InRange(backend.PrepareCoverageCallCount, 1, 20); + Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs new file mode 100644 index 000000000..6910f7586 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +[GroupOutput("Drawing")] +public class WebGPUDrawingBackendTests +{ + [Theory] + [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] + public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(18, 28) + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = "Sphinx of black quartz, judge my vow\n0123456789"; + Brush brush = Brushes.Solid(Color.Black); + Pen pen = Pens.Solid(Color.OrangeRed, 2F); + + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + defaultImage.DebugSave( + provider, + "DefaultBackend_DrawText", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = provider.GetImage(); + using WebGPUDrawingBackend backend = new(); + webGpuImage.Configuration.SetDrawingBackend(backend); + webGpuImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + + webGpuImage.DebugSave( + provider, + "WebGPUBackend_DrawText", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.True(backend.PrepareCoverageCallCount > 0); + Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + + ImageComparer comparer = ImageComparer.TolerantPercentage(4F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + + [Theory] + [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + + using Image image = provider.GetImage(); + using WebGPUDrawingBackend backend = new(); + image.Configuration.SetDrawingBackend(backend); + + image.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); + + image.DebugSave( + provider, + "WebGPUBackend_RepeatedGlyphs", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.InRange(backend.PrepareCoverageCallCount, 1, 20); + Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + } + + private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backend) + { + Assert.Equal( + backend.PrepareCoverageCallCount, + backend.GpuPrepareCoverageCallCount + backend.FallbackPrepareCoverageCallCount); + Assert.Equal( + backend.CompositeCoverageCallCount, + backend.GpuCompositeCoverageCallCount + backend.CpuCompositeCoverageCallCount); + } + + private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) + { + bool requireGpuPath = string.Equals( + Environment.GetEnvironmentVariable("IMAGESHARP_REQUIRE_WEBGPU"), + "1", + StringComparison.Ordinal); + + if (!requireGpuPath) + { + return; + } + + Assert.True( + backend.IsGpuReady, + $"WebGPU initialization did not succeed. Reason='{backend.LastGpuInitializationFailure}'. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}, Composite(total/gpu/cpu)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.CpuCompositeCoverageCallCount}"); + Assert.True( + backend.GpuPrepareCoverageCallCount > 0, + $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}"); + Assert.True( + backend.GpuCompositeCoverageCallCount > 0, + $"No GPU composite calls were observed. Composite(total/gpu/cpu)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.CpuCompositeCoverageCallCount}"); + Assert.Equal( + 0, + backend.FallbackPrepareCoverageCallCount); + Assert.Equal( + 0, + backend.CpuCompositeCoverageCallCount); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs index 0d49fa0e4..f2d71700b 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs @@ -190,6 +190,9 @@ public void DrawPathProcessor_UsesNonZeroRule_WhenStrokeNormalizationIsDisabled( FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); Assert.Equal(IntersectionRule.NonZero, definition.Options.ShapeOptions.IntersectionRule); + Assert.Equal( + RasterizerSamplingOrigin.PixelCenter, + definition.GetProtectedValue("SamplingOrigin")); } [Fact] @@ -215,6 +218,9 @@ public void DrawPathProcessor_PreservesRule_WhenStrokeNormalizationIsEnabled() FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); Assert.Equal(IntersectionRule.EvenOdd, definition.Options.ShapeOptions.IntersectionRule); + Assert.Equal( + RasterizerSamplingOrigin.PixelCenter, + definition.GetProtectedValue("SamplingOrigin")); } [Fact] diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index ffea506c6..3effa1a4a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -32,7 +32,7 @@ public void GetDefaultDrawingBackendFromConfiguration_AlwaysReturnsDefaultInstan IDrawingBackend second = configuration.GetDrawingBackend(); Assert.Same(first, second); - Assert.Same(CpuDrawingBackend.Instance, first); + Assert.Same(DefaultDrawingBackend.Instance, first); } [Fact] @@ -44,7 +44,7 @@ public void SetRasterizerOnConfiguration_RoundTrips() configuration.SetRasterizer(rasterizer); Assert.Same(rasterizer, configuration.GetRasterizer()); - Assert.IsType(configuration.GetDrawingBackend()); + Assert.IsType(configuration.GetDrawingBackend()); } [Fact] @@ -57,7 +57,7 @@ public void SetRasterizerOnProcessingContext_RoundTrips() context.SetRasterizer(rasterizer); Assert.Same(rasterizer, context.GetRasterizer()); - Assert.IsType(context.GetDrawingBackend()); + Assert.IsType(context.GetDrawingBackend()); } [Fact] @@ -109,24 +109,68 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { + public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + } + + public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + where TPixel : unmanaged, IPixel + { + } + public void FillPath( Configuration configuration, - ImageFrame source, + Buffer2DRegion target, IPath path, Brush brush, - in GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - Rectangle brushBounds, - MemoryAllocator allocator) + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + where TPixel : unmanaged, IPixel + { + } + + public void FillRegion( + Configuration configuration, + Buffer2DRegion target, + Brush brush, + GraphicsOptions graphicsOptions, + Rectangle region) + where TPixel : unmanaged, IPixel + { + } + + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) where TPixel : unmanaged, IPixel { + _ = brush; + _ = graphicsOptions; + return true; } - public void RasterizeCoverage( + public DrawingCoverageHandle PrepareCoverage( IPath path, in RasterizerOptions rasterizerOptions, MemoryAllocator allocator, - Buffer2D destination) + CoveragePreparationMode preparationMode) + { + _ = preparationMode; + return default; + } + + public void CompositeCoverage( + Configuration configuration, + Buffer2DRegion target, + DrawingCoverageHandle coverageHandle, + Point sourceOffset, + Brush brush, + in GraphicsOptions graphicsOptions, + Rectangle brushBounds) + where TPixel : unmanaged, IPixel + { + } + + public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) { } } From 7ea5866e63c9f47a37a7c5f298427e90f38c604b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 20 Feb 2026 14:10:35 +1000 Subject: [PATCH 002/136] We have a prototype! --- .../ImageSharp.Drawing.WebGPU.csproj | 19 +- .../Shaders/CoverageRasterizationShader.cs | 1 - .../WebGPUDrawingBackend.cs | 80 +++++-- .../WebGpuRuntime.cs | 226 ++++++++++++++++++ .../Backends/DefaultDrawingBackend.cs | 17 +- .../Processing/DrawingCanvas{TPixel}.cs | 4 +- .../Drawing/DrawPolygon.cs | 14 ++ .../ImageSharp.Drawing.Benchmarks.csproj | 1 + 8 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index 14b43d012..fae29af27 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -17,6 +17,13 @@ false + + + $(WarningsNotAsErrors);8002 @@ -49,9 +56,15 @@ - - - + + + + diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs index f158b35cd..d9c050b8b 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs @@ -89,7 +89,6 @@ fn single_sample(pixel: vec2) -> f32 { fn antialias_sample(pixel: vec2) -> f32 { // Supersample a fixed grid around the configured sample origin. - // This produces smoother coverage than the previous 2x2 tap pattern. let grid: u32 = 8u; let inv_sample_count = 1.0 / f32(grid * grid); let origin = vec2(params.sample_origin_x, params.sample_origin_y); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index b0fc6f67c..59256719e 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -28,12 +28,17 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private static ReadOnlySpan EntryPointFragment => "fs_main\0"u8; + private static readonly byte[] CompositeShaderCode = CreateNullTerminatedUtf8(CompositeCoverageShader.Code); + + private static readonly byte[] CoverageShaderCode = CreateNullTerminatedUtf8(CoverageRasterizationShader.Code); + private readonly object gpuSync = new(); private readonly ConcurrentDictionary preparedCoverage = new(); private readonly DefaultDrawingBackend fallbackBackend; private int nextCoverageHandleId; private bool isDisposed; + private WebGpuRuntime.Lease? runtimeLease; private WebGPU? webGpu; private Wgpu? wgpuExtension; private Instance* instance; @@ -503,6 +508,11 @@ public void Dispose() foreach (KeyValuePair kv in this.preparedCoverage) { + if (kv.Value.IsFallback) + { + this.fallbackBackend.ReleaseCoverage(kv.Value.FallbackCoverageHandle); + } + this.ReleaseCoverageTextureLocked(kv.Value); kv.Value.Dispose(); } @@ -559,8 +569,9 @@ private bool TryInitializeGpuLocked() Trace("TryInitializeGpuLocked: begin"); try { - this.webGpu = WebGPU.GetApi(); - _ = this.webGpu.TryGetDeviceExtension(null, out this.wgpuExtension); + this.runtimeLease = WebGpuRuntime.Acquire(); + this.webGpu = this.runtimeLease.Api; + this.wgpuExtension = this.runtimeLease.WgpuExtension; Trace($"TryInitializeGpuLocked: extension={(this.wgpuExtension is null ? "none" : "wgpu.h")}"); this.instance = this.webGpu.CreateInstance((InstanceDescriptor*)null); if (this.instance is null) @@ -785,8 +796,7 @@ private bool TryCreateCompositePipelineLocked() ShaderModule* shaderModule = null; try { - ReadOnlySpan shaderCode = CompositeCoverageShader.Code; - fixed (byte* shaderCodePtr = shaderCode) + fixed (byte* shaderCodePtr = CompositeShaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() { @@ -950,8 +960,7 @@ private bool TryCreateCoveragePipelineLocked() ShaderModule* shaderModule = null; try { - ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; - fixed (byte* shaderCodePtr = shaderCode) + fixed (byte* shaderCodePtr = CoverageShaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() { @@ -1221,10 +1230,6 @@ this.coverageBindGroupLayout is null || } this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - if (this.wgpuExtension is not null) - { - _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); - } this.webGpu.CommandBufferRelease(commandBuffer); commandBuffer = null; @@ -1300,6 +1305,14 @@ private static void AddEdge(PointF from, PointF to, float offsetX, float offsetY return; } + if (!float.IsFinite(from.X) || + !float.IsFinite(from.Y) || + !float.IsFinite(to.X) || + !float.IsFinite(to.Y)) + { + return; + } + destination.Add(new EdgeData { X0 = from.X + offsetX, @@ -1841,10 +1854,6 @@ coverageEntry.GpuCoverageView is null || } this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - if (this.wgpuExtension is not null) - { - _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); - } this.webGpu.CommandBufferRelease(commandBuffer); commandBuffer = null; @@ -1983,6 +1992,16 @@ private void ReleaseCoverageTextureLocked(CoverageEntry entry) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte[] CreateNullTerminatedUtf8(ReadOnlySpan text) + { + byte[] buffer = new byte[text.Length + 1]; + Span destination = buffer.AsSpan(); + text.CopyTo(destination); + destination[text.Length] = 0; + return buffer; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) where T : struct @@ -2032,6 +2051,30 @@ private void ReleaseBufferLocked(WgpuBuffer* buffer) this.webGpu.BufferRelease(buffer); } + private void TryDestroyAndDrainDeviceLocked() + { + if (this.webGpu is null || this.device is null) + { + return; + } + + this.webGpu.DeviceDestroy(this.device); + + if (this.wgpuExtension is not null) + { + // Drain native callbacks/work queues before releasing the device and unloading. + _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); + _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); + return; + } + + if (this.instance is not null) + { + this.webGpu.InstanceProcessEvents(this.instance); + this.webGpu.InstanceProcessEvents(this.instance); + } + } + private void ReleaseGpuResourcesLocked() { Trace("ReleaseGpuResourcesLocked: begin"); @@ -2075,6 +2118,11 @@ private void ReleaseGpuResourcesLocked() this.compositeBindGroupLayout = null; } + if (this.device is not null) + { + this.TryDestroyAndDrainDeviceLocked(); + } + if (this.queue is not null) { this.webGpu.QueueRelease(this.queue); @@ -2099,10 +2147,12 @@ private void ReleaseGpuResourcesLocked() this.instance = null; } - this.webGpu.Dispose(); this.webGpu = null; } + this.wgpuExtension = null; + this.runtimeLease?.Dispose(); + this.runtimeLease = null; this.IsGpuReady = false; this.compositeSessionGpuActive = false; this.compositeSessionDepth = 0; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs new file mode 100644 index 000000000..1d1efce09 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Process-level WebGPU API runtime. +/// +/// +/// +/// This type owns the process-level Silk API loader and its +/// optional extension. +/// +/// +/// Backends acquire access by taking a via . +/// The lease count is thread-safe and prevents accidental shutdown while active +/// backends are still running. +/// +/// +/// Runtime unload is explicit: +/// +/// +/// when there are no active leases. +/// Best-effort cleanup on process exit. +/// +/// +/// The shutdown path is resilient to duplicate native unload attempts. +/// +/// +internal static unsafe class WebGpuRuntime +{ + /// + /// Synchronizes all runtime state transitions. + /// + private static readonly object Sync = new(); + + /// + /// Process-level WebGPU API loader. + /// + private static WebGPU? api; + + /// + /// Optional wgpu-native extension facade. + /// + private static Wgpu? wgpuExtension; + + /// + /// Number of currently active runtime leases. + /// + private static int leaseCount; + + /// + /// Tracks whether the process-exit hook has been installed. + /// + private static bool processExitHooked; + + /// + /// Acquires a runtime lease for WebGPU access. + /// + /// A lease that must be disposed when access is no longer required. + /// Thrown when the WebGPU API cannot be initialized. + public static Lease Acquire() + { + lock (Sync) + { + if (!processExitHooked) + { + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + processExitHooked = true; + } + + api ??= WebGPU.GetApi(); + if (api is null) + { + throw new InvalidOperationException("WebGPU.GetApi returned null."); + } + + if (wgpuExtension is null) + { + _ = api.TryGetDeviceExtension(null, out wgpuExtension); + } + + leaseCount++; + return new Lease(api, wgpuExtension); + } + } + + /// + /// Releases one active runtime lease. + /// + /// + /// Lease release does not automatically unload the runtime. Unload is performed by + /// or by the process-exit handler. + /// + private static void Release() + { + lock (Sync) + { + if (leaseCount <= 0) + { + return; + } + + leaseCount--; + } + } + + /// + /// Shuts down the process-level WebGPU runtime when no leases are active. + /// + /// + /// This call is intended for coordinated application shutdown. Runtime state can be + /// reinitialized later by calling again. + /// + /// Thrown when runtime leases are still active. + public static void Shutdown() + { + lock (Sync) + { + if (leaseCount != 0) + { + throw new InvalidOperationException($"Cannot shut down WebGPU runtime while {leaseCount} lease(s) are active."); + } + + DisposeRuntimeCore(); + } + } + + /// + /// Process-exit cleanup callback. + /// + /// Event sender. + /// Event arguments. + private static void OnProcessExit(object? sender, EventArgs e) + { + _ = sender; + _ = e; + lock (Sync) + { + leaseCount = 0; + DisposeRuntimeCore(); + } + } + + /// + /// Disposes native runtime objects in a safe and idempotent way. + /// + /// + /// Duplicate-dispose exceptions are intentionally swallowed because process-exit + /// teardown may race with other shutdown paths. + /// + private static void DisposeRuntimeCore() + { + try + { + wgpuExtension?.Dispose(); + } + catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) + { + // Safe to ignore at process shutdown or double-dispose races. + } + finally + { + wgpuExtension = null; + } + + try + { + api?.Dispose(); + } + catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) + { + // Safe to ignore at process shutdown or double-dispose races. + } + finally + { + api = null; + } + } + + /// + /// Ref-counted access token for . + /// + /// + /// Disposing the lease decrements the runtime lease count exactly once. + /// + internal sealed class Lease : IDisposable + { + private int disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The shared WebGPU API loader. + /// The shared optional wgpu extension facade. + internal Lease(WebGPU api, Wgpu? wgpuExtension) + { + this.Api = api; + this.WgpuExtension = wgpuExtension; + } + + /// + /// Gets the shared WebGPU API loader. + /// + public WebGPU Api { get; } + + /// + /// Gets the shared optional wgpu extension facade. + /// + public Wgpu? WgpuExtension { get; } + + /// + /// Releases this lease exactly once. + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.disposed, 1) == 0) + { + Release(); + } + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 0fdaaae7c..779e23ceb 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -124,7 +124,7 @@ public DrawingCoverageHandle PrepareCoverage( Buffer2D destination = allocator.Allocate2D(size, AllocationOptions.Clean); - CoverageRasterizationState state = new(destination); + CoverageRasterizationState state = new(destination, rasterizerOptions.Interest.Top); this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessCoverageScanline); int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); @@ -365,7 +365,8 @@ private static void ProcessRasterizedScanline(int y, Span scanlin /// Callback state containing destination storage. private static void ProcessCoverageScanline(int y, Span scanline, ref CoverageRasterizationState state) { - Span destination = state.Buffer.DangerousGetRowSpan(y); + int row = y - state.DestinationTop; + Span destination = state.Buffer.DangerousGetRowSpan(row); scanline.CopyTo(destination); } @@ -498,12 +499,22 @@ private readonly struct CoverageRasterizationState /// Initializes a new instance of the struct. /// /// Destination coverage buffer. - public CoverageRasterizationState(Buffer2D buffer) => this.Buffer = buffer; + /// Absolute Y corresponding to destination row 0. + public CoverageRasterizationState(Buffer2D buffer, int destinationTop) + { + this.Buffer = buffer; + this.DestinationTop = destinationTop; + } /// /// Gets the destination coverage buffer. /// public Buffer2D Buffer { get; } + + /// + /// Gets the absolute Y corresponding to destination row 0. + /// + public int DestinationTop { get; } } /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 1a9d74fa6..ad464d601 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -257,12 +257,12 @@ private void DrawTextOperations(IEnumerable operations, Drawin } finally { + this.backend.EndCompositeSession(this.configuration, this.targetRegion); + foreach ((_, CoverageCacheEntry coverageEntry) in coverageCache) { this.backend.ReleaseCoverage(coverageEntry.CoverageHandle); } - - this.backend.EndCompositeSession(this.configuration, this.targetRegion); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index c3080014a..32a0c22e4 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -8,6 +8,7 @@ using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; @@ -23,6 +24,7 @@ public abstract class DrawPolygon private PointF[][] points; private Image image; + private Image webGpuImage; private Bitmap sdBitmap; private Graphics sdGraphics; @@ -38,6 +40,8 @@ public abstract class DrawPolygon private IPath imageSharpPath; private IPath strokedImageSharpPath; + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; protected abstract int Width { get; } @@ -107,6 +111,10 @@ public void Setup() this.image = new Image(this.Width, this.Height); this.isPen = new SolidPen(Color.White, this.Thickness); this.strokedImageSharpPath = this.isPen.GeneratePath(this.imageSharpPath); + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + this.webGpuImage = new Image(this.webGpuConfiguration, this.Width, this.Height); this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -148,6 +156,8 @@ public void Cleanup() this.skPath.Dispose(); this.image.Dispose(); + this.webGpuImage.Dispose(); + this.webGpuBackend.Dispose(); } [Benchmark] @@ -177,6 +187,10 @@ public void ImageSharpSeparatePathsScanlineRasterizer() public void ImageSharpCombinedPathsTiled() => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] + public void ImageSharpCombinedPathsWebGpuBackend() + => this.webGpuImage.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + [Benchmark] public void ImageSharpSeparatePathsTiled() => this.image.Mutate( diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 0a2f32ce1..f85acbe30 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -40,6 +40,7 @@ + From 08fb876ba3356defcb91595088687b11a830f83c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 20 Feb 2026 19:45:07 +1000 Subject: [PATCH 003/136] Refactor WebGPU drawing backend and shaders --- .../Shaders/CompositeCoverageShader.cs | 11 +- .../Shaders/CoverageRasterizationShader.cs | 115 +- .../WebGPUDrawingBackend.cs | 1221 +++++++++++++---- .../Backends/DefaultDrawingBackend.cs | 1 + .../Drawing/DrawPolygon.cs | 3 + .../Backends/WebGPUDrawingBackendTests.cs | 105 +- 6 files changed, 1076 insertions(+), 380 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs index 73379cae9..4a7dd541d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs @@ -5,8 +5,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal static class CompositeCoverageShader { - public static ReadOnlySpan Code => - """ + private static readonly byte[] CodeBytes = + [ + .. """ struct CompositeParams { source_offset_x: u32, source_offset_y: u32, @@ -97,5 +98,9 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4 { return vec4(brush.rgb * source_alpha, source_alpha); } - """u8; + """u8, + .. "\0"u8 + ]; + + public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs index d9c050b8b..4377879fa 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs @@ -5,38 +5,28 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal static class CoverageRasterizationShader { - public static ReadOnlySpan Code => - """ - struct Edge { - x0: f32, - y0: f32, - x1: f32, - y1: f32, - }; - - struct CoverageParams { - edge_count: u32, - intersection_rule: u32, - antialias: u32, - _pad0: u32, - sample_origin_x: f32, - sample_origin_y: f32, - _pad1: f32, - _pad2: f32, - }; - - @group(0) @binding(0) - var edges: array; - - @group(0) @binding(1) - var params: CoverageParams; - + private static readonly byte[] CodeBytes = + [ + .. """ struct VertexOutput { @builtin(position) position: vec4, }; @vertex - fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + fn vs_edge(@location(0) position: vec2) -> VertexOutput { + var output: VertexOutput; + output.position = vec4(position, 0.0, 1.0); + return output; + } + + @fragment + fn fs_stencil() -> @location(0) vec4 { + // Color writes are disabled for the stencil pipeline. + return vec4(0.0, 0.0, 0.0, 0.0); + } + + @vertex + fn vs_cover(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { var positions = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), @@ -47,70 +37,13 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return output; } - fn is_inside(sample: vec2) -> bool { - var winding: i32 = 0; - var crossings: u32 = 0u; - - for (var i: u32 = 0u; i < params.edge_count; i = i + 1u) { - let edge = edges[i]; - if (edge.y0 == edge.y1) { - continue; - } - - let upward = (edge.y0 <= sample.y) && (edge.y1 > sample.y); - let downward = (edge.y0 > sample.y) && (edge.y1 <= sample.y); - if (!(upward || downward)) { - continue; - } - - let t = (sample.y - edge.y0) / (edge.y1 - edge.y0); - let x = edge.x0 + t * (edge.x1 - edge.x0); - if (x > sample.x) { - crossings = crossings + 1u; - if (upward) { - winding = winding + 1; - } else { - winding = winding - 1; - } - } - } - - if (params.intersection_rule == 0u) { - return (crossings & 1u) == 1u; - } - - return winding != 0; - } - - fn single_sample(pixel: vec2) -> f32 { - let sample = pixel + vec2(params.sample_origin_x, params.sample_origin_y); - return select(0.0, 1.0, is_inside(sample)); - } - - fn antialias_sample(pixel: vec2) -> f32 { - // Supersample a fixed grid around the configured sample origin. - let grid: u32 = 8u; - let inv_sample_count = 1.0 / f32(grid * grid); - let origin = vec2(params.sample_origin_x, params.sample_origin_y); - let base = origin - vec2(0.5, 0.5); - - var covered = 0.0; - for (var y: u32 = 0u; y < grid; y = y + 1u) { - let fy = (f32(y) + 0.5) / f32(grid); - for (var x: u32 = 0u; x < grid; x = x + 1u) { - let fx = (f32(x) + 0.5) / f32(grid); - covered = covered + select(0.0, 1.0, is_inside(pixel + base + vec2(fx, fy))); - } - } - - return covered * inv_sample_count; - } - @fragment - fn fs_main(@builtin(position) position: vec4) -> @location(0) vec4 { - let pixel = floor(position.xy); - let coverage = select(single_sample(pixel), antialias_sample(pixel), params.antialias != 0u); - return vec4(coverage, 0.0, 0.0, 1.0); + fn fs_cover() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); } - """u8; + """u8, + .. "\0"u8 + ]; + + public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 59256719e..01298109f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,19 +17,89 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; #pragma warning disable SA1201 // Elements should appear in the correct order +/// +/// WebGPU-backed implementation of . +/// +/// +/// +/// This backend intentionally preserves the contract used by +/// processors and DrawingCanvas<TPixel>. The public flow is identical to the default +/// backend: +/// +/// +/// Prepare path coverage into a reusable handle. +/// Composite prepared coverage into a target region using brush + graphics options. +/// Release coverage handle resources deterministically. +/// +/// +/// The implementation detail differs: coverage preparation is accelerated through WebGPU render +/// passes while composition uses a dedicated blend shader targeting Rgba8Unorm. +/// +/// +/// Internally, the backend is split into two independent phases: +/// +/// +/// +/// Coverage preparation: +/// path geometry is flattened in local-interest coordinates, converted to edge triangles, +/// then rasterized by a stencil-and-cover render pass into an R8Unorm coverage mask. +/// This avoids per-pixel edge scans in shader code. +/// +/// +/// Coverage composition: +/// a composition shader samples the prepared coverage mask and applies brush/blend rules into +/// an Rgba8Unorm target texture using source-over semantics. +/// +/// +/// +/// Coverage rasterization supports both fill rules: +/// and . +/// The active rule selects the appropriate stencil pipeline at draw time. +/// +/// +/// Composition runs in session mode: +/// the target region is uploaded once, multiple composite operations execute on the same GPU +/// texture, then one readback copies results to the destination buffer. +/// +/// +/// Threading model: all GPU object creation, command encoding, submission, and map/readback are +/// synchronized by . This keeps native resource lifetime deterministic and +/// prevents command submission races while still allowing concurrent high-level calls. +/// +/// +/// Handle ownership model: prepared coverage is stored in and owned +/// by this backend instance. The caller receives only an opaque . +/// Releasing the handle always releases the corresponding GPU texture/view (or fallback handle). +/// +/// +/// Sampling model: path geometry is translated to local interest space and adjusted for +/// before rasterization so coverage generation remains +/// consistent with canvas-local coordinate semantics. +/// +/// +/// If a GPU path is unavailable for the current operation (unsupported pixel/brush/blend mode +/// or initialization failure), behavior falls back to so +/// output remains deterministic and API semantics stay consistent. +/// +/// internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; - private const uint CoverageVertexCount = 3; + private const uint CoverageCoverVertexCount = 3; + private const uint CoverageSampleCount = 4; private const int CallbackTimeoutMilliseconds = 10_000; - private static ReadOnlySpan EntryPointVertex => "vs_main\0"u8; + private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; - private static ReadOnlySpan EntryPointFragment => "fs_main\0"u8; + private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; - private static readonly byte[] CompositeShaderCode = CreateNullTerminatedUtf8(CompositeCoverageShader.Code); + private static ReadOnlySpan CoverageStencilVertexEntryPoint => "vs_edge\0"u8; - private static readonly byte[] CoverageShaderCode = CreateNullTerminatedUtf8(CoverageRasterizationShader.Code); + private static ReadOnlySpan CoverageStencilFragmentEntryPoint => "fs_stencil\0"u8; + + private static ReadOnlySpan CoverageCoverVertexEntryPoint => "vs_cover\0"u8; + + private static ReadOnlySpan CoverageCoverFragmentEntryPoint => "fs_cover\0"u8; private readonly object gpuSync = new(); private readonly ConcurrentDictionary preparedCoverage = new(); @@ -48,9 +117,11 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private BindGroupLayout* compositeBindGroupLayout; private PipelineLayout* compositePipelineLayout; private RenderPipeline* compositePipeline; - private BindGroupLayout* coverageBindGroupLayout; private PipelineLayout* coveragePipelineLayout; - private RenderPipeline* coveragePipeline; + private RenderPipeline* coverageStencilEvenOddPipeline; + private RenderPipeline* coverageStencilNonZeroIncrementPipeline; + private RenderPipeline* coverageStencilNonZeroDecrementPipeline; + private RenderPipeline* coverageCoverPipeline; private int compositeSessionDepth; private bool compositeSessionGpuActive; @@ -59,8 +130,11 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private Texture* compositeSessionTargetTexture; private TextureView* compositeSessionTargetView; private WgpuBuffer* compositeSessionReadbackBuffer; + private CommandEncoder* compositeSessionCommandEncoder; private uint compositeSessionReadbackBytesPerRow; private ulong compositeSessionReadbackByteCount; + private int compositeSessionResourceWidth; + private int compositeSessionResourceHeight; private static readonly bool TraceEnabled = string.Equals( Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), "1", @@ -76,28 +150,69 @@ private static void Trace(string message) } } + /// + /// Gets the total number of coverage preparation requests. + /// public int PrepareCoverageCallCount { get; private set; } + /// + /// Gets the number of coverage preparations executed on the GPU. + /// public int GpuPrepareCoverageCallCount { get; private set; } + /// + /// Gets the number of coverage preparations delegated to the fallback backend. + /// public int FallbackPrepareCoverageCallCount { get; private set; } + /// + /// Gets the total number of composition requests. + /// public int CompositeCoverageCallCount { get; private set; } + /// + /// Gets the number of compositions executed on the GPU. + /// public int GpuCompositeCoverageCallCount { get; private set; } - public int CpuCompositeCoverageCallCount { get; private set; } + /// + /// Gets the number of compositions delegated to the fallback backend. + /// + public int FallbackCompositeCoverageCallCount { get; private set; } + /// + /// Gets the number of released coverage handles. + /// public int ReleaseCoverageCallCount { get; private set; } + /// + /// Gets a value indicating whether the backend completed GPU initialization. + /// public bool IsGpuReady { get; private set; } + /// + /// Gets a value indicating whether GPU initialization has been attempted. + /// public bool GpuInitializationAttempted { get; private set; } + /// + /// Gets the last GPU initialization failure reason, if any. + /// public string? LastGpuInitializationFailure { get; private set; } + /// + /// Gets the number of prepared coverage entries currently cached by handle. + /// public int LiveCoverageCount => this.preparedCoverage.Count; + /// + /// Begins a composite session for a target region. + /// + /// + /// Nested calls are reference-counted. The first successful call uploads the target + /// pixels into a GPU texture. The final matching + /// flushes GPU results back to the target. + /// public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) where TPixel : unmanaged, IPixel { @@ -133,6 +248,14 @@ public void BeginCompositeSession(Configuration configuration, Buffer2DR } } + /// + /// Ends a previously started composite session. + /// + /// + /// When this is the outermost session and GPU work has modified the session texture, + /// the method performs one readback into the destination region, then clears active + /// session state. Session textures/buffers can be retained and reused by later sessions. + /// public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) where TPixel : unmanaged, IPixel { @@ -159,13 +282,20 @@ public void EndCompositeSession(Configuration configuration, Buffer2DReg this.TryFlushCompositeSessionLocked(); } - this.ReleaseCompositeSessionLocked(); + this.ResetCompositeSessionStateLocked(); } this.compositeSessionGpuActive = false; this.compositeSessionDirty = false; } + /// + /// Fills a path on the specified target region. + /// + /// + /// The method clips interest bounds to the local target region, prepares reusable coverage, + /// then composites that coverage with the supplied brush. + /// public void FillPath( Configuration configuration, Buffer2DRegion target, @@ -207,11 +337,24 @@ public void FillPath( ? CoveragePreparationMode.Default : CoveragePreparationMode.Fallback; + long prepareStart = 0; + if (TraceEnabled) + { + prepareStart = Stopwatch.GetTimestamp(); + } + DrawingCoverageHandle coverageHandle = this.PrepareCoverage( path, clippedOptions, configuration.MemoryAllocator, preparationMode); + + if (TraceEnabled) + { + double prepareMs = Stopwatch.GetElapsedTime(prepareStart).TotalMilliseconds; + Trace($"FillPath: prepare={prepareMs:F3}ms mode={preparationMode}"); + } + if (!coverageHandle.IsValid) { return; @@ -220,16 +363,45 @@ public void FillPath( try { Buffer2DRegion compositeTarget = target.GetSubRegion(clippedInterest); + bool openedCompositeSession = false; + if (preparationMode == CoveragePreparationMode.Default && this.compositeSessionDepth == 0) + { + this.BeginCompositeSession(configuration, compositeTarget); + openedCompositeSession = true; + } + Rectangle brushBounds = Rectangle.Ceiling(path.Bounds); - this.CompositeCoverage( - configuration, - compositeTarget, - coverageHandle, - Point.Empty, - brush, - graphicsOptions, - brushBounds); + try + { + long compositeStart = 0; + if (TraceEnabled) + { + compositeStart = Stopwatch.GetTimestamp(); + } + + this.CompositeCoverage( + configuration, + compositeTarget, + coverageHandle, + Point.Empty, + brush, + graphicsOptions, + brushBounds); + + if (TraceEnabled) + { + double compositeMs = Stopwatch.GetElapsedTime(compositeStart).TotalMilliseconds; + Trace($"FillPath: composite={compositeMs:F3}ms"); + } + } + finally + { + if (openedCompositeSession) + { + this.EndCompositeSession(configuration, compositeTarget); + } + } } finally { @@ -237,6 +409,13 @@ public void FillPath( } } + /// + /// Fills a rectangular region on the specified target region. + /// + /// + /// Rect fills are normalized through + /// so both APIs share the same coverage and composition paths. + /// public void FillRegion( Configuration configuration, Buffer2DRegion target, @@ -288,6 +467,9 @@ public void FillRegion( rasterizerOptions); } + /// + /// Determines whether this backend can composite coverage with the given brush/options. + /// public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) where TPixel : unmanaged, IPixel { @@ -295,10 +477,16 @@ public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions return CanUseGpuComposite(graphicsOptions) && WebGpuBrushData.TryCreate(brush, out _) - && this.TryEnsureGpuReady() - && this.compositeSessionGpuActive; + && this.TryEnsureGpuReady(); } + /// + /// Prepares coverage for a path and returns an opaque reusable handle. + /// + /// + /// GPU preparation flattens path edges into local-interest coordinates, builds a tiled edge index, + /// and rasterizes the coverage texture. Unsupported scenarios delegate to fallback preparation. + /// public DrawingCoverageHandle PrepareCoverage( IPath path, in RasterizerOptions rasterizerOptions, @@ -326,7 +514,12 @@ public DrawingCoverageHandle PrepareCoverage( return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); } - if (!TryBuildEdges(path, rasterizerOptions.Interest.Location, out EdgeData[]? edges) || edges.Length == 0) + if (!TryBuildCoverageTriangles( + path, + rasterizerOptions.Interest.Location, + rasterizerOptions.Interest.Size, + rasterizerOptions.SamplingOrigin, + out CoverageTriangleData coverageTriangleData)) { return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); } @@ -339,9 +532,15 @@ public DrawingCoverageHandle PrepareCoverage( this.webGpu is null || this.device is null || this.queue is null || - this.coveragePipeline is null || - this.coverageBindGroupLayout is null || - !this.TryRasterizeCoverageTextureLocked(edges, in rasterizerOptions, out coverageTexture, out coverageView)) + this.coverageStencilEvenOddPipeline is null || + this.coverageStencilNonZeroIncrementPipeline is null || + this.coverageStencilNonZeroDecrementPipeline is null || + this.coverageCoverPipeline is null || + !this.TryRasterizeCoverageTextureLocked( + coverageTriangleData, + in rasterizerOptions, + out coverageTexture, + out coverageView)) { return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); } @@ -401,6 +600,14 @@ private DrawingCoverageHandle PrepareCoverageFallback( return new DrawingCoverageHandle(handleId); } + /// + /// Composes prepared coverage into a target region using the provided brush. + /// + /// + /// Handles prepared in fallback mode are always composed by the fallback backend. + /// Handles prepared in accelerated mode must be composed in accelerated mode. + /// Mixed-mode fallback is deliberately disabled to keep behavior explicit. + /// public void CompositeCoverage( Configuration configuration, Buffer2DRegion target, @@ -429,7 +636,7 @@ public void CompositeCoverage( if (entry.IsFallback) { - this.CpuCompositeCoverageCallCount++; + this.FallbackCompositeCoverageCallCount++; this.fallbackBackend.CompositeCoverage( configuration, target, @@ -441,14 +648,24 @@ public void CompositeCoverage( return; } - if (!CanUseGpuComposite(graphicsOptions) || - !WebGpuBrushData.TryCreate(brush, out WebGpuBrushData brushData) || - !this.TryEnsureGpuReady()) + if (!CanUseGpuComposite(graphicsOptions) || !this.TryEnsureGpuReady()) + { + throw new InvalidOperationException( + "Mixed-mode coverage composition is disabled. Coverage was prepared for accelerated composition, but the current composite settings are not GPU-supported."); + } + + if (!WebGpuBrushData.TryCreate(brush, out WebGpuBrushData brushData)) { throw new InvalidOperationException( "Mixed-mode coverage composition is disabled. Coverage was prepared for accelerated composition, but the current composite settings are not GPU-supported."); } + if (!this.compositeSessionGpuActive || this.compositeSessionDepth <= 0) + { + throw new InvalidOperationException( + "Accelerated coverage composition requires an active composite session."); + } + Buffer2DRegion rgbaTarget = Unsafe.As, Buffer2DRegion>(ref target); if (!this.TryCompositeCoverageGpu( rgbaTarget, @@ -464,6 +681,9 @@ public void CompositeCoverage( this.GpuCompositeCoverageCallCount++; } + /// + /// Releases a previously prepared coverage handle. + /// public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) { this.ReleaseCoverageCallCount++; @@ -489,6 +709,9 @@ public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) } } + /// + /// Releases all cached coverage and GPU resources owned by this backend instance. + /// public void Dispose() { if (this.isDisposed) @@ -504,7 +727,8 @@ public void Dispose() this.TryFlushCompositeSessionLocked(); } - this.ReleaseCompositeSessionLocked(); + this.ResetCompositeSessionStateLocked(); + this.ReleaseCompositeSessionResourcesLocked(); foreach (KeyValuePair kv in this.preparedCoverage) { @@ -538,6 +762,13 @@ private static bool CanUseGpuSession() where TPixel : unmanaged, IPixel => typeof(TPixel) == typeof(Rgba32); + /// + /// Ensures this instance has a ready-to-use GPU device/pipeline set. + /// + /// + /// Initialization is single-attempt per backend instance; subsequent calls are + /// cheap and return cached state. + /// private bool TryEnsureGpuReady() { if (this.IsGpuReady) @@ -564,6 +795,9 @@ private bool TryEnsureGpuReady() } } + /// + /// Performs one-time GPU initialization while is held. + /// private bool TryInitializeGpuLocked() { Trace("TryInitializeGpuLocked: begin"); @@ -636,9 +870,11 @@ private bool TryInitializeGpuLocked() (this.compositePipeline is null || this.compositePipelineLayout is null || this.compositeBindGroupLayout is null || - this.coveragePipeline is null || + this.coverageStencilEvenOddPipeline is null || + this.coverageStencilNonZeroIncrementPipeline is null || + this.coverageStencilNonZeroDecrementPipeline is null || + this.coverageCoverPipeline is null || this.coveragePipelineLayout is null || - this.coverageBindGroupLayout is null || this.device is null || this.queue is null)) { @@ -737,6 +973,9 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, v return true; } + /// + /// Creates the render pipeline used for coverage composition. + /// private bool TryCreateCompositePipelineLocked() { if (this.webGpu is null || this.device is null) @@ -796,7 +1035,8 @@ private bool TryCreateCompositePipelineLocked() ShaderModule* shaderModule = null; try { - fixed (byte* shaderCodePtr = CompositeShaderCode) + ReadOnlySpan shaderCode = CompositeCoverageShader.Code; + fixed (byte* shaderCodePtr = shaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() { @@ -820,8 +1060,8 @@ private bool TryCreateCompositePipelineLocked() return false; } - ReadOnlySpan vertexEntryPoint = EntryPointVertex; - ReadOnlySpan fragmentEntryPoint = EntryPointFragment; + ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; + ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; fixed (byte* vertexEntryPointPtr = vertexEntryPoint) { fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) @@ -902,6 +1142,9 @@ private bool TryCreateCompositePipelineLocked() } } + /// + /// Creates the render pipeline used for coverage rasterization. + /// private bool TryCreateCoveragePipelineLocked() { if (this.webGpu is null || this.device is null) @@ -909,46 +1152,10 @@ private bool TryCreateCoveragePipelineLocked() return false; } - BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; - layoutEntries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - MinBindingSize = 16 - } - }; - layoutEntries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - MinBindingSize = (ulong)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor layoutDescriptor = new() - { - EntryCount = 2, - Entries = layoutEntries - }; - - this.coverageBindGroupLayout = this.webGpu.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); - if (this.coverageBindGroupLayout is null) - { - return false; - } - - BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; - bindGroupLayouts[0] = this.coverageBindGroupLayout; PipelineLayoutDescriptor pipelineLayoutDescriptor = new() { - BindGroupLayoutCount = 1, - BindGroupLayouts = bindGroupLayouts + BindGroupLayoutCount = 0, + BindGroupLayouts = null }; this.coveragePipelineLayout = this.webGpu.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); @@ -960,7 +1167,8 @@ private bool TryCreateCoveragePipelineLocked() ShaderModule* shaderModule = null; try { - fixed (byte* shaderCodePtr = CoverageShaderCode) + ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; + fixed (byte* shaderCodePtr = shaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() { @@ -984,40 +1192,246 @@ private bool TryCreateCoveragePipelineLocked() return false; } - ReadOnlySpan vertexEntryPoint = EntryPointVertex; - ReadOnlySpan fragmentEntryPoint = EntryPointFragment; - fixed (byte* vertexEntryPointPtr = vertexEntryPoint) + ReadOnlySpan stencilVertexEntryPoint = CoverageStencilVertexEntryPoint; + ReadOnlySpan stencilFragmentEntryPoint = CoverageStencilFragmentEntryPoint; + ReadOnlySpan coverVertexEntryPoint = CoverageCoverVertexEntryPoint; + ReadOnlySpan coverFragmentEntryPoint = CoverageCoverFragmentEntryPoint; + fixed (byte* stencilVertexEntryPointPtr = stencilVertexEntryPoint) { - fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + fixed (byte* stencilFragmentEntryPointPtr = stencilFragmentEntryPoint) { - VertexState vertexState = new() + VertexAttribute* stencilVertexAttributes = stackalloc VertexAttribute[1]; + stencilVertexAttributes[0] = new VertexAttribute + { + Format = VertexFormat.Float32x2, + Offset = 0, + ShaderLocation = 0 + }; + + VertexBufferLayout* stencilVertexBuffers = stackalloc VertexBufferLayout[1]; + stencilVertexBuffers[0] = new VertexBufferLayout + { + ArrayStride = (ulong)Unsafe.SizeOf(), + StepMode = VertexStepMode.Vertex, + AttributeCount = 1, + Attributes = stencilVertexAttributes + }; + + VertexState stencilVertexState = new() { Module = shaderModule, - EntryPoint = vertexEntryPointPtr, + EntryPoint = stencilVertexEntryPointPtr, + BufferCount = 1, + Buffers = stencilVertexBuffers + }; + + ColorTargetState* stencilColorTargets = stackalloc ColorTargetState[1]; + stencilColorTargets[0] = new ColorTargetState + { + Format = TextureFormat.R8Unorm, + Blend = null, + WriteMask = ColorWriteMask.None + }; + + FragmentState stencilFragmentState = new() + { + Module = shaderModule, + EntryPoint = stencilFragmentEntryPointPtr, + TargetCount = 1, + Targets = stencilColorTargets + }; + + PrimitiveState primitiveState = new() + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }; + + MultisampleState multisampleState = new() + { + Count = CoverageSampleCount, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }; + + StencilFaceState evenOddStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.Invert + }; + + DepthStencilState evenOddDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = evenOddStencilFace, + StencilBack = evenOddStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor evenOddPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = primitiveState, + DepthStencil = &evenOddDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilEvenOddPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in evenOddPipelineDescriptor); + if (this.coverageStencilEvenOddPipeline is null) + { + return false; + } + + StencilFaceState incrementStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.IncrementWrap + }; + + DepthStencilState incrementDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = incrementStencilFace, + StencilBack = incrementStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + PrimitiveState incrementPrimitiveState = primitiveState; + incrementPrimitiveState.CullMode = CullMode.Back; + + RenderPipelineDescriptor incrementPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = incrementPrimitiveState, + DepthStencil = &incrementDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilNonZeroIncrementPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in incrementPipelineDescriptor); + if (this.coverageStencilNonZeroIncrementPipeline is null) + { + return false; + } + + StencilFaceState decrementStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.DecrementWrap + }; + + DepthStencilState decrementDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = decrementStencilFace, + StencilBack = decrementStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + PrimitiveState decrementPrimitiveState = primitiveState; + decrementPrimitiveState.CullMode = CullMode.Front; + + RenderPipelineDescriptor decrementPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = decrementPrimitiveState, + DepthStencil = &decrementDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilNonZeroDecrementPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in decrementPipelineDescriptor); + if (this.coverageStencilNonZeroDecrementPipeline is null) + { + return false; + } + } + } + + fixed (byte* coverVertexEntryPointPtr = coverVertexEntryPoint) + { + fixed (byte* coverFragmentEntryPointPtr = coverFragmentEntryPoint) + { + VertexState coverVertexState = new() + { + Module = shaderModule, + EntryPoint = coverVertexEntryPointPtr, BufferCount = 0, Buffers = null }; - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; - colorTargets[0] = new ColorTargetState + ColorTargetState* coverColorTargets = stackalloc ColorTargetState[1]; + coverColorTargets[0] = new ColorTargetState { Format = TextureFormat.R8Unorm, Blend = null, WriteMask = ColorWriteMask.Red }; - FragmentState fragmentState = new() + FragmentState coverFragmentState = new() { Module = shaderModule, - EntryPoint = fragmentEntryPointPtr, + EntryPoint = coverFragmentEntryPointPtr, TargetCount = 1, - Targets = colorTargets + Targets = coverColorTargets }; - RenderPipelineDescriptor pipelineDescriptor = new() + StencilFaceState coverStencilFace = new() + { + Compare = CompareFunction.NotEqual, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.Keep + }; + + DepthStencilState coverDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = coverStencilFace, + StencilBack = coverStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = 0, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor coverPipelineDescriptor = new() { Layout = this.coveragePipelineLayout, - Vertex = vertexState, + Vertex = coverVertexState, Primitive = new PrimitiveState { Topology = PrimitiveTopology.TriangleList, @@ -1025,21 +1439,21 @@ private bool TryCreateCoveragePipelineLocked() FrontFace = FrontFace.Ccw, CullMode = CullMode.None }, - DepthStencil = null, + DepthStencil = &coverDepthStencilState, Multisample = new MultisampleState { - Count = 1, + Count = CoverageSampleCount, Mask = uint.MaxValue, AlphaToCoverageEnabled = false }, - Fragment = &fragmentState + Fragment = &coverFragmentState }; - this.coveragePipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + this.coverageCoverPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in coverPipelineDescriptor); } } - return this.coveragePipeline is not null; + return this.coverageCoverPipeline is not null; } finally { @@ -1050,143 +1464,151 @@ private bool TryCreateCoveragePipelineLocked() } } + /// + /// Rasterizes edge triangles through a stencil-and-cover pass into an R8Unorm texture. + /// private bool TryRasterizeCoverageTextureLocked( - ReadOnlySpan edges, + in CoverageTriangleData coverageTriangleData, in RasterizerOptions rasterizerOptions, out Texture* coverageTexture, out TextureView* coverageView) { - Trace($"TryRasterizeCoverageTextureLocked: begin edges={edges.Length} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); + Trace($"TryRasterizeCoverageTextureLocked: begin triangles={coverageTriangleData.Vertices.Length / 3} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); coverageTexture = null; coverageView = null; if (this.webGpu is null || this.device is null || this.queue is null || - this.coveragePipeline is null || - this.coverageBindGroupLayout is null || - edges.Length == 0 || + this.coverageStencilEvenOddPipeline is null || + this.coverageStencilNonZeroIncrementPipeline is null || + this.coverageStencilNonZeroDecrementPipeline is null || + this.coverageCoverPipeline is null || + coverageTriangleData.Vertices.Length == 0 || rasterizerOptions.Interest.Width <= 0 || rasterizerOptions.Interest.Height <= 0) { return false; } - TextureDescriptor coverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = 1 - }; - - coverageTexture = this.webGpu.DeviceCreateTexture(this.device, in coverageTextureDescriptor); - if (coverageTexture is null) - { - return false; - } - - TextureViewDescriptor coverageViewDescriptor = new() - { - Format = TextureFormat.R8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - coverageView = this.webGpu.TextureCreateView(coverageTexture, in coverageViewDescriptor); - if (coverageView is null) - { - this.ReleaseTextureLocked(coverageTexture); - coverageTexture = null; - return false; - } - - ulong edgesBufferSize = checked((ulong)edges.Length * (ulong)Unsafe.SizeOf()); - ulong paramsBufferSize = (ulong)Unsafe.SizeOf(); - WgpuBuffer* edgesBuffer = null; - WgpuBuffer* paramsBuffer = null; - BindGroup* bindGroup = null; + Texture* createdCoverageTexture = null; + TextureView* createdCoverageView = null; + Texture* multisampleCoverageTexture = null; + TextureView* multisampleCoverageView = null; + Texture* stencilTexture = null; + TextureView* stencilView = null; + WgpuBuffer* vertexBuffer = null; CommandEncoder* commandEncoder = null; RenderPassEncoder* passEncoder = null; CommandBuffer* commandBuffer = null; + bool success = false; try { - BufferDescriptor edgesBufferDescriptor = new() + TextureDescriptor coverageTextureDescriptor = new() { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = edgesBufferSize + Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = 1 }; - edgesBuffer = this.webGpu.DeviceCreateBuffer(this.device, in edgesBufferDescriptor); - if (edgesBuffer is null) + + createdCoverageTexture = this.webGpu.DeviceCreateTexture(this.device, in coverageTextureDescriptor); + if (createdCoverageTexture is null) { return false; } - BufferDescriptor paramsBufferDescriptor = new() + TextureViewDescriptor coverageViewDescriptor = new() { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = paramsBufferSize + Format = TextureFormat.R8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + createdCoverageView = this.webGpu.TextureCreateView(createdCoverageTexture, in coverageViewDescriptor); + if (createdCoverageView is null) + { + return false; + } + + TextureDescriptor multisampleCoverageTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = CoverageSampleCount }; - paramsBuffer = this.webGpu.DeviceCreateBuffer(this.device, in paramsBufferDescriptor); - if (paramsBuffer is null) + + multisampleCoverageTexture = this.webGpu.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); + if (multisampleCoverageTexture is null) { return false; } - fixed (EdgeData* edgesPtr = edges) + multisampleCoverageView = this.webGpu.TextureCreateView(multisampleCoverageTexture, in coverageViewDescriptor); + if (multisampleCoverageView is null) { - this.webGpu.QueueWriteBuffer(this.queue, edgesBuffer, 0, edgesPtr, (nuint)edgesBufferSize); + return false; } - CoverageParams coverageParams = new() + TextureDescriptor stencilTextureDescriptor = new() { - EdgeCount = (uint)edges.Length, - IntersectionRule = rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 0U : 1U, - Antialias = rasterizerOptions.RasterizationMode == RasterizationMode.Antialiased ? 1U : 0U, - SampleOriginX = rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F, - SampleOriginY = rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), + Format = TextureFormat.Depth24PlusStencil8, + MipLevelCount = 1, + SampleCount = CoverageSampleCount }; - this.webGpu.QueueWriteBuffer( - this.queue, - paramsBuffer, - 0, - ref coverageParams, - (nuint)Unsafe.SizeOf()); - BindGroupEntry* bindEntries = stackalloc BindGroupEntry[2]; - bindEntries[0] = new BindGroupEntry + stencilTexture = this.webGpu.DeviceCreateTexture(this.device, in stencilTextureDescriptor); + if (stencilTexture is null) { - Binding = 0, - Buffer = edgesBuffer, - Offset = 0, - Size = edgesBufferSize - }; - bindEntries[1] = new BindGroupEntry + return false; + } + + TextureViewDescriptor stencilViewDescriptor = new() { - Binding = 1, - Buffer = paramsBuffer, - Offset = 0, - Size = paramsBufferSize + Format = TextureFormat.Depth24PlusStencil8, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All }; - BindGroupDescriptor bindGroupDescriptor = new() + stencilView = this.webGpu.TextureCreateView(stencilTexture, in stencilViewDescriptor); + if (stencilView is null) { - Layout = this.coverageBindGroupLayout, - EntryCount = 2, - Entries = bindEntries + return false; + } + + ulong vertexByteCount = checked((ulong)coverageTriangleData.Vertices.Length * (ulong)Unsafe.SizeOf()); + BufferDescriptor vertexBufferDescriptor = new() + { + Usage = BufferUsage.Vertex | BufferUsage.CopyDst, + Size = vertexByteCount }; - bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); - if (bindGroup is null) + vertexBuffer = this.webGpu.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); + if (vertexBuffer is null) { return false; } + fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) + { + this.webGpu.QueueWriteBuffer(this.queue, vertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); + } + CommandEncoderDescriptor commandEncoderDescriptor = default; commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); if (commandEncoder is null) @@ -1196,17 +1618,31 @@ this.coverageBindGroupLayout is null || RenderPassColorAttachment colorAttachment = new() { - View = coverageView, - ResolveTarget = null, + View = multisampleCoverageView, + ResolveTarget = createdCoverageView, LoadOp = LoadOp.Clear, - StoreOp = StoreOp.Store, + StoreOp = StoreOp.Discard, ClearValue = default }; + RenderPassDepthStencilAttachment depthStencilAttachment = new() + { + View = stencilView, + DepthLoadOp = LoadOp.Clear, + DepthStoreOp = StoreOp.Discard, + DepthClearValue = 1F, + DepthReadOnly = false, + StencilLoadOp = LoadOp.Clear, + StencilStoreOp = StoreOp.Discard, + StencilClearValue = 0, + StencilReadOnly = false + }; + RenderPassDescriptor renderPassDescriptor = new() { ColorAttachmentCount = 1, - ColorAttachments = &colorAttachment + ColorAttachments = &colorAttachment, + DepthStencilAttachment = &depthStencilAttachment }; passEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); @@ -1215,9 +1651,26 @@ this.coverageBindGroupLayout is null || return false; } - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coveragePipeline); - this.webGpu.RenderPassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, (uint*)null); - this.webGpu.RenderPassEncoderDraw(passEncoder, CoverageVertexCount, 1, 0, 0); + this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); + this.webGpu.RenderPassEncoderSetVertexBuffer(passEncoder, 0, vertexBuffer, 0, vertexByteCount); + if (rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd) + { + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilEvenOddPipeline); + this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + } + else + { + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); + this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); + this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + } + + this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageCoverPipeline); + this.webGpu.RenderPassEncoderDraw(passEncoder, CoverageCoverVertexCount, 1, 0, 0); + this.webGpu.RenderPassEncoderEnd(passEncoder); this.webGpu.RenderPassEncoderRelease(passEncoder); passEncoder = null; @@ -1233,6 +1686,11 @@ this.coverageBindGroupLayout is null || this.webGpu.CommandBufferRelease(commandBuffer); commandBuffer = null; + coverageTexture = createdCoverageTexture; + coverageView = createdCoverageView; + createdCoverageTexture = null; + createdCoverageView = null; + success = true; Trace("TryRasterizeCoverageTextureLocked: submitted"); return true; } @@ -1253,21 +1711,44 @@ this.coverageBindGroupLayout is null || this.webGpu.CommandEncoderRelease(commandEncoder); } - if (bindGroup is not null) + this.ReleaseBufferLocked(vertexBuffer); + this.ReleaseTextureViewLocked(stencilView); + this.ReleaseTextureLocked(stencilTexture); + this.ReleaseTextureViewLocked(multisampleCoverageView); + this.ReleaseTextureLocked(multisampleCoverageTexture); + + if (!success) { - this.webGpu.BindGroupRelease(bindGroup); + this.ReleaseTextureViewLocked(createdCoverageView); + this.ReleaseTextureLocked(createdCoverageTexture); } - - this.ReleaseBufferLocked(paramsBuffer); - this.ReleaseBufferLocked(edgesBuffer); } } - private static bool TryBuildEdges(IPath path, Point interestLocation, [NotNullWhen(true)] out EdgeData[]? edges) + /// + /// Flattens a path into local-interest coordinates and converts each edge to a triangle + /// anchored at an external origin. These triangles are consumed by the stencil pass. + /// + private static bool TryBuildCoverageTriangles( + IPath path, + Point interestLocation, + Size interestSize, + RasterizerSamplingOrigin samplingOrigin, + out CoverageTriangleData coverageTriangleData) { - List edgeList = []; - float offsetX = -interestLocation.X; - float offsetY = -interestLocation.Y; + coverageTriangleData = default; + if (interestSize.Width <= 0 || interestSize.Height <= 0) + { + return false; + } + + float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelBoundary ? 0.5F : 0F; + float offsetX = sampleShift - interestLocation.X; + float offsetY = sampleShift - interestLocation.Y; + + List segments = []; + float minX = float.PositiveInfinity; + float minY = float.PositiveInfinity; foreach (ISimplePath simplePath in path.Flatten()) { @@ -1279,26 +1760,46 @@ private static bool TryBuildEdges(IPath path, Point interestLocation, [NotNullWh for (int i = 1; i < points.Length; i++) { - AddEdge(points[i - 1], points[i], offsetX, offsetY, edgeList); + AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX, ref minY); } if (simplePath.IsClosed) { - AddEdge(points[^1], points[0], offsetX, offsetY, edgeList); + AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX, ref minY); } } - if (edgeList.Count == 0) + if (segments.Count == 0 || !float.IsFinite(minX) || !float.IsFinite(minY)) { - edges = null; return false; } - edges = [.. edgeList]; + float originX = minX - 1F; + float originY = minY - 1F; + float widthScale = 2F / interestSize.Width; + float heightScale = 2F / interestSize.Height; + + StencilVertex[] vertices = new StencilVertex[checked(segments.Count * 3)]; + int vertexIndex = 0; + foreach (CoverageSegment segment in segments) + { + vertices[vertexIndex++] = ToStencilVertex(originX, originY, widthScale, heightScale); + vertices[vertexIndex++] = ToStencilVertex(segment.FromX, segment.FromY, widthScale, heightScale); + vertices[vertexIndex++] = ToStencilVertex(segment.ToX, segment.ToY, widthScale, heightScale); + } + + coverageTriangleData = new CoverageTriangleData(vertices); return true; } - private static void AddEdge(PointF from, PointF to, float offsetX, float offsetY, List destination) + private static void AddCoverageSegment( + PointF from, + PointF to, + float offsetX, + float offsetY, + List destination, + ref float minX, + ref float minY) { if (from.Equals(to)) { @@ -1313,19 +1814,31 @@ private static void AddEdge(PointF from, PointF to, float offsetX, float offsetY return; } - destination.Add(new EdgeData + float fromX = from.X + offsetX; + float fromY = from.Y + offsetY; + float toX = to.X + offsetX; + float toY = to.Y + offsetY; + + destination.Add(new CoverageSegment(fromX, fromY, toX, toY)); + minX = MathF.Min(minX, MathF.Min(fromX, toX)); + minY = MathF.Min(minY, MathF.Min(fromY, toY)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static StencilVertex ToStencilVertex(float x, float y, float widthScale, float heightScale) + { + return new StencilVertex { - X0 = from.X + offsetX, - Y0 = from.Y + offsetY, - X1 = to.X + offsetX, - Y1 = to.Y + offsetY - }); + X = (x * widthScale) - 1F, + Y = 1F - (y * heightScale) + }; } private bool WaitForSignalLocked(ManualResetEventSlim signal) { Stopwatch timer = Stopwatch.StartNew(); - while (!signal.Wait(1)) + SpinWait spinner = default; + while (!signal.IsSet) { if (timer.ElapsedMilliseconds >= CallbackTimeoutMilliseconds) { @@ -1342,6 +1855,18 @@ private bool WaitForSignalLocked(ManualResetEventSlim signal) { this.webGpu.InstanceProcessEvents(this.instance); } + + if (!signal.IsSet) + { + if (spinner.Count < 10) + { + spinner.SpinOnce(); + } + else + { + Thread.Yield(); + } + } } return true; @@ -1365,7 +1890,11 @@ private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTextur Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); - if (IsSingleMemory(sourceRegion.Buffer)) + // For full-row regions in a contiguous buffer, upload directly with source stride. + // For subregions, prefer tightly packed upload to avoid transferring row gaps. + if (IsSingleMemory(sourceRegion.Buffer) && + sourceRegion.Rectangle.X == 0 && + sourceRegion.Width == sourceRegion.Buffer.Width) { int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); @@ -1423,10 +1952,11 @@ private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTextur } } + /// + /// Ensures session resources for the target size, then uploads target pixels once. + /// private bool TryBeginCompositeSessionLocked(Buffer2DRegion target) { - this.ReleaseCompositeSessionLocked(); - if (!this.IsGpuReady || this.webGpu is null || this.device is null || @@ -1437,15 +1967,54 @@ this.queue is null || return false; } - uint textureRowBytes = checked((uint)target.Width * (uint)Unsafe.SizeOf()); + if (!this.TryEnsureCompositeSessionResourcesLocked(target.Width, target.Height) || + this.compositeSessionTargetTexture is null) + { + return false; + } + + this.ResetCompositeSessionStateLocked(); + if (!this.TryQueueWriteTextureFromRgbaRegionLocked(this.compositeSessionTargetTexture, target)) + { + return false; + } + + this.compositeSessionTarget = target; + this.compositeSessionDirty = false; + return true; + } + + private bool TryEnsureCompositeSessionResourcesLocked(int width, int height) + { + if (!this.IsGpuReady || + this.webGpu is null || + this.device is null || + width <= 0 || + height <= 0) + { + return false; + } + + if (this.compositeSessionTargetTexture is not null && + this.compositeSessionTargetView is not null && + this.compositeSessionReadbackBuffer is not null && + this.compositeSessionResourceWidth == width && + this.compositeSessionResourceHeight == height) + { + return true; + } + + this.ReleaseCompositeSessionResourcesLocked(); + + uint textureRowBytes = checked((uint)width * (uint)Unsafe.SizeOf()); uint readbackRowBytes = AlignTo256(textureRowBytes); - ulong readbackByteCount = (ulong)readbackRowBytes * (uint)target.Height; + ulong readbackByteCount = (ulong)readbackRowBytes * (uint)height; TextureDescriptor targetTextureDescriptor = new() { Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)target.Width, (uint)target.Height, 1), + Size = new Extent3D((uint)width, (uint)height, 1), Format = TextureFormat.Rgba8Unorm, MipLevelCount = 1, SampleCount = 1 @@ -1484,29 +2053,24 @@ this.queue is null || WgpuBuffer* readbackBuffer = this.webGpu.DeviceCreateBuffer(this.device, in readbackBufferDescriptor); if (readbackBuffer is null) { - this.ReleaseBufferLocked(readbackBuffer); - this.ReleaseTextureViewLocked(targetView); - this.ReleaseTextureLocked(targetTexture); - return false; - } - - if (!this.TryQueueWriteTextureFromRgbaRegionLocked(targetTexture, target)) - { - this.ReleaseBufferLocked(readbackBuffer); this.ReleaseTextureViewLocked(targetView); this.ReleaseTextureLocked(targetTexture); return false; } - this.compositeSessionTarget = target; this.compositeSessionTargetTexture = targetTexture; this.compositeSessionTargetView = targetView; this.compositeSessionReadbackBuffer = readbackBuffer; this.compositeSessionReadbackBytesPerRow = readbackRowBytes; this.compositeSessionReadbackByteCount = readbackByteCount; + this.compositeSessionResourceWidth = width; + this.compositeSessionResourceHeight = height; return true; } + /// + /// Reads the session target texture back into the canvas region. + /// private bool TryFlushCompositeSessionLocked() { Trace("TryFlushCompositeSessionLocked: begin"); @@ -1523,15 +2087,19 @@ this.compositeSessionReadbackBuffer is null || return false; } - CommandEncoder* commandEncoder = null; + CommandEncoder* commandEncoder = this.compositeSessionCommandEncoder; + bool usingSessionCommandEncoder = commandEncoder is not null; CommandBuffer* commandBuffer = null; try { - CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); if (commandEncoder is null) { - return false; + CommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (commandEncoder is null) + { + return false; + } } ImageCopyTexture source = new() @@ -1563,6 +2131,8 @@ this.compositeSessionReadbackBuffer is null || return false; } + this.compositeSessionCommandEncoder = null; + this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); this.webGpu.CommandBufferRelease(commandBuffer); commandBuffer = null; @@ -1581,6 +2151,11 @@ this.compositeSessionReadbackBuffer is null || } finally { + if (usingSessionCommandEncoder) + { + this.compositeSessionCommandEncoder = null; + } + if (commandBuffer is not null) { this.webGpu.CommandBufferRelease(commandBuffer); @@ -1593,8 +2168,26 @@ this.compositeSessionReadbackBuffer is null || } } - private void ReleaseCompositeSessionLocked() + private void ResetCompositeSessionStateLocked() + { + if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) + { + this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); + this.compositeSessionCommandEncoder = null; + } + + this.compositeSessionTarget = default; + this.compositeSessionDirty = false; + } + + private void ReleaseCompositeSessionResourcesLocked() { + if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) + { + this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); + this.compositeSessionCommandEncoder = null; + } + this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); this.ReleaseTextureViewLocked(this.compositeSessionTargetView); this.ReleaseTextureLocked(this.compositeSessionTargetTexture); @@ -1603,8 +2196,8 @@ private void ReleaseCompositeSessionLocked() this.compositeSessionTargetView = null; this.compositeSessionReadbackBytesPerRow = 0; this.compositeSessionReadbackByteCount = 0; - this.compositeSessionTarget = default; - this.compositeSessionDirty = false; + this.compositeSessionResourceWidth = 0; + this.compositeSessionResourceHeight = 0; } private bool TryCompositeCoverageGpu( @@ -1651,7 +2244,7 @@ private bool TryCompositeCoverageGpu( lock (this.gpuSync) { if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || - this.compositePipeline is null || this.compositeBindGroupLayout is null || this.compositeSessionTargetView is null) + this.compositePipeline is null || this.compositeBindGroupLayout is null) { return false; } @@ -1680,11 +2273,20 @@ this.compositeSessionTargetTexture is not null && return true; } - if (this.TryRunCompositePassInSessionLocked( + if (!this.TryEnsureCompositeSessionCommandEncoderLocked()) + { + return false; + } + + if (this.TryRunCompositePassLocked( + this.compositeSessionCommandEncoder, entry, sourceOffset, brushData, blendPercentage, + this.compositeSessionTargetView, + this.compositeSessionTarget.Width, + this.compositeSessionTarget.Height, destinationX, destinationY, sessionCompositeWidth, @@ -1699,7 +2301,8 @@ this.compositeSessionTargetTexture is not null && this.TryFlushCompositeSessionLocked(); } - this.ReleaseCompositeSessionLocked(); + this.ResetCompositeSessionStateLocked(); + this.ReleaseCompositeSessionResourcesLocked(); this.compositeSessionGpuActive = false; return false; } @@ -1708,6 +2311,23 @@ this.compositeSessionTargetTexture is not null && } } + private bool TryEnsureCompositeSessionCommandEncoderLocked() + { + if (this.compositeSessionCommandEncoder is not null) + { + return true; + } + + if (this.webGpu is null || this.device is null) + { + return false; + } + + CommandEncoderDescriptor commandEncoderDescriptor = default; + this.compositeSessionCommandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + return this.compositeSessionCommandEncoder is not null; + } + private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) { if (entry.GpuCoverageTexture is not null && entry.GpuCoverageView is not null) @@ -1718,11 +2338,18 @@ private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) return false; } - private bool TryRunCompositePassInSessionLocked( + /// + /// Executes one composition draw call into the session target texture. + /// + private bool TryRunCompositePassLocked( + CommandEncoder* commandEncoder, CoverageEntry coverageEntry, Point sourceOffset, WebGpuBrushData brushData, float blendPercentage, + TextureView* targetView, + int targetWidth, + int targetHeight, int destinationX, int destinationY, int compositeWidth, @@ -1734,7 +2361,9 @@ this.queue is null || this.compositePipeline is null || this.compositeBindGroupLayout is null || coverageEntry.GpuCoverageView is null || - this.compositeSessionTargetView is null) + targetView is null || + targetWidth <= 0 || + targetHeight <= 0) { return false; } @@ -1747,7 +2376,7 @@ coverageEntry.GpuCoverageView is null || ulong uniformByteCount = (ulong)Unsafe.SizeOf(); WgpuBuffer* uniformBuffer = null; BindGroup* bindGroup = null; - CommandEncoder* commandEncoder = null; + CommandEncoder* createdCommandEncoder = null; RenderPassEncoder* passEncoder = null; CommandBuffer* commandBuffer = null; try @@ -1771,8 +2400,8 @@ coverageEntry.GpuCoverageView is null || DestinationY = (uint)destinationY, DestinationWidth = (uint)compositeWidth, DestinationHeight = (uint)compositeHeight, - TargetWidth = (uint)this.compositeSessionTarget.Width, - TargetHeight = (uint)this.compositeSessionTarget.Height, + TargetWidth = (uint)targetWidth, + TargetHeight = (uint)targetHeight, BrushKind = (uint)brushData.Kind, SolidBrushColor = brushData.SolidColor, BlendPercentage = blendPercentage @@ -1811,16 +2440,22 @@ coverageEntry.GpuCoverageView is null || return false; } - CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); - if (commandEncoder is null) + CommandEncoder* compositeCommandEncoder = commandEncoder; + if (compositeCommandEncoder is null) { - return false; + CommandEncoderDescriptor commandEncoderDescriptor = default; + createdCommandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (createdCommandEncoder is null) + { + return false; + } + + compositeCommandEncoder = createdCommandEncoder; } RenderPassColorAttachment colorAttachment = new() { - View = this.compositeSessionTargetView, + View = targetView, ResolveTarget = null, LoadOp = LoadOp.Load, StoreOp = StoreOp.Store, @@ -1833,7 +2468,7 @@ coverageEntry.GpuCoverageView is null || ColorAttachments = &colorAttachment }; - passEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + passEncoder = this.webGpu.CommandEncoderBeginRenderPass(compositeCommandEncoder, in renderPassDescriptor); if (passEncoder is null) { return false; @@ -1846,8 +2481,13 @@ coverageEntry.GpuCoverageView is null || this.webGpu.RenderPassEncoderRelease(passEncoder); passEncoder = null; + if (createdCommandEncoder is null) + { + return true; + } + CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + commandBuffer = this.webGpu.CommandEncoderFinish(createdCommandEncoder, in commandBufferDescriptor); if (commandBuffer is null) { return false; @@ -1871,9 +2511,9 @@ coverageEntry.GpuCoverageView is null || this.webGpu.CommandBufferRelease(commandBuffer); } - if (commandEncoder is not null) + if (createdCommandEncoder is not null) { - this.webGpu.CommandEncoderRelease(commandEncoder); + this.webGpu.CommandEncoderRelease(createdCommandEncoder); } if (bindGroup is not null) @@ -1992,16 +2632,6 @@ private void ReleaseCoverageTextureLocked(CoverageEntry entry) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte[] CreateNullTerminatedUtf8(ReadOnlySpan text) - { - byte[] buffer = new byte[text.Length + 1]; - Span destination = buffer.AsSpan(); - text.CopyTo(destination); - destination[text.Length] = 0; - return buffer; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) where T : struct @@ -2078,26 +2708,39 @@ private void TryDestroyAndDrainDeviceLocked() private void ReleaseGpuResourcesLocked() { Trace("ReleaseGpuResourcesLocked: begin"); - this.ReleaseCompositeSessionLocked(); + this.ResetCompositeSessionStateLocked(); + this.ReleaseCompositeSessionResourcesLocked(); if (this.webGpu is not null) { - if (this.coveragePipeline is not null) + if (this.coverageCoverPipeline is not null) { - this.webGpu.RenderPipelineRelease(this.coveragePipeline); - this.coveragePipeline = null; + this.webGpu.RenderPipelineRelease(this.coverageCoverPipeline); + this.coverageCoverPipeline = null; } - if (this.coveragePipelineLayout is not null) + if (this.coverageStencilNonZeroDecrementPipeline is not null) { - this.webGpu.PipelineLayoutRelease(this.coveragePipelineLayout); - this.coveragePipelineLayout = null; + this.webGpu.RenderPipelineRelease(this.coverageStencilNonZeroDecrementPipeline); + this.coverageStencilNonZeroDecrementPipeline = null; + } + + if (this.coverageStencilNonZeroIncrementPipeline is not null) + { + this.webGpu.RenderPipelineRelease(this.coverageStencilNonZeroIncrementPipeline); + this.coverageStencilNonZeroIncrementPipeline = null; + } + + if (this.coverageStencilEvenOddPipeline is not null) + { + this.webGpu.RenderPipelineRelease(this.coverageStencilEvenOddPipeline); + this.coverageStencilEvenOddPipeline = null; } - if (this.coverageBindGroupLayout is not null) + if (this.coveragePipelineLayout is not null) { - this.webGpu.BindGroupLayoutRelease(this.coverageBindGroupLayout); - this.coverageBindGroupLayout = null; + this.webGpu.PipelineLayoutRelease(this.coveragePipelineLayout); + this.coveragePipelineLayout = null; } if (this.compositePipeline is not null) @@ -2186,25 +2829,39 @@ private struct CompositeParams } [StructLayout(LayoutKind.Sequential)] - private struct CoverageParams + private struct StencilVertex { - public uint EdgeCount; - public uint IntersectionRule; - public uint Antialias; - public uint Padding0; - public float SampleOriginX; - public float SampleOriginY; - public float Padding1; - public float Padding2; + public float X; + public float Y; } - [StructLayout(LayoutKind.Sequential)] - private struct EdgeData + private readonly struct CoverageSegment { - public float X0; - public float Y0; - public float X1; - public float Y1; + public CoverageSegment(float fromX, float fromY, float toX, float toY) + { + this.FromX = fromX; + this.FromY = fromY; + this.ToX = toX; + this.ToY = toY; + } + + public float FromX { get; } + + public float FromY { get; } + + public float ToX { get; } + + public float ToY { get; } + } + + private readonly struct CoverageTriangleData + { + public CoverageTriangleData(StencilVertex[] vertices) + { + this.Vertices = vertices; + } + + public StencilVertex[] Vertices { get; } } private sealed class CoverageEntry : IDisposable diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 779e23ceb..c3c58e229 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -258,6 +258,7 @@ private static void FillPath( graphicsOptions, destinationRegion, path.Bounds); + FillRasterizationState state = new( destinationRegion, applicator, diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 32a0c22e4..470377a61 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -211,6 +211,9 @@ public void SkiaSharp() [Benchmark] public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); + + [Benchmark] + public void FillPolygonWebGpuBackend() => this.webGpuImage.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); } public class DrawPolygonAll : DrawPolygon diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 6910f7586..82ba75673 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -2,8 +2,10 @@ // Licensed under the Six Labors Split License. using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -13,6 +15,101 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; [GroupOutput("Drawing")] public class WebGPUDrawingBackendTests { + [Theory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); + Brush brush = Brushes.Solid(Color.Black); + + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + + using Image webGpuImage = provider.GetImage(); + using WebGPUDrawingBackend backend = new(); + webGpuImage.Configuration.SetDrawingBackend(backend); + webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + + Assert.True(backend.PrepareCoverageCallCount > 0); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + AssertCoverageExecutionAccounting(backend); + if (backend.IsGpuReady) + { + Assert.True(backend.GpuPrepareCoverageCallCount > 0); + Assert.True(backend.GpuCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount > 0); + } + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true }, + ShapeOptions = new ShapeOptions + { + IntersectionRule = IntersectionRule.NonZero + } + }; + + PathBuilder pathBuilder = new(); + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(16, 16), + new PointF(240, 16), + new PointF(240, 240), + new PointF(16, 240) + ]); + pathBuilder.CloseFigure(); + + // Inner contour keeps the same winding direction as outer contour. + // Non-zero fill should therefore keep this region filled. + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(80, 80), + new PointF(176, 80), + new PointF(176, 176), + new PointF(80, 176) + ]); + pathBuilder.CloseFigure(); + + IPath path = pathBuilder.Build(); + Brush brush = Brushes.Solid(Color.Black); + + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); + + using Image webGpuImage = provider.GetImage(); + using WebGPUDrawingBackend backend = new(); + webGpuImage.Configuration.SetDrawingBackend(backend); + webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); + + Assert.True(backend.PrepareCoverageCallCount > 0); + Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); + Assert.Equal(0, backend.LiveCoverageCount); + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + + // WebGPU and CPU rasterization differ slightly on edge coverage quantization, + // but non-zero winding semantics must still match. + Assert.Equal(defaultImage[128, 128], webGpuImage[128, 128]); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + [Theory] [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) @@ -108,7 +205,7 @@ private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backe backend.GpuPrepareCoverageCallCount + backend.FallbackPrepareCoverageCallCount); Assert.Equal( backend.CompositeCoverageCallCount, - backend.GpuCompositeCoverageCallCount + backend.CpuCompositeCoverageCallCount); + backend.GpuCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount); } private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) @@ -125,18 +222,18 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) Assert.True( backend.IsGpuReady, - $"WebGPU initialization did not succeed. Reason='{backend.LastGpuInitializationFailure}'. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}, Composite(total/gpu/cpu)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.CpuCompositeCoverageCallCount}"); + $"WebGPU initialization did not succeed. Reason='{backend.LastGpuInitializationFailure}'. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}, Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); Assert.True( backend.GpuPrepareCoverageCallCount > 0, $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}"); Assert.True( backend.GpuCompositeCoverageCallCount > 0, - $"No GPU composite calls were observed. Composite(total/gpu/cpu)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.CpuCompositeCoverageCallCount}"); + $"No GPU composite calls were observed. Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); Assert.Equal( 0, backend.FallbackPrepareCoverageCallCount); Assert.Equal( 0, - backend.CpuCompositeCoverageCallCount); + backend.FallbackCompositeCoverageCallCount); } } From ac16b612e9950a7df47650e1553d0aa809eb4e43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 20 Feb 2026 21:13:09 +1000 Subject: [PATCH 004/136] Support multiple pixel fomats --- .../WebGPUDrawingBackend.CompositePixels.cs | 66 +++ .../WebGPUDrawingBackend.cs | 424 ++++++++++++------ 2 files changed, 349 insertions(+), 141 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs new file mode 100644 index 000000000..efc379b81 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Pixel-format registration for composite session I/O. +/// +internal sealed partial class WebGPUDrawingBackend +{ + private static Dictionary CreateCompositePixelHandlers() => + + // No-swizzle mappings only. Unsupported types are intentionally omitted from this map. + new() + { + [typeof(A8)] = CompositePixelRegistration.Create(TextureFormat.R8Unorm), + [typeof(L8)] = CompositePixelRegistration.Create(TextureFormat.R8Unorm), + [typeof(La16)] = CompositePixelRegistration.Create(TextureFormat.RG8Unorm), + + [typeof(Byte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Uint), + [typeof(NormalizedByte2)] = CompositePixelRegistration.Create(TextureFormat.RG8Snorm), + [typeof(NormalizedByte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Snorm), + + [typeof(HalfSingle)] = CompositePixelRegistration.Create(TextureFormat.R16float), + [typeof(HalfVector2)] = CompositePixelRegistration.Create(TextureFormat.RG16float), + [typeof(HalfVector4)] = CompositePixelRegistration.Create(TextureFormat.Rgba16float), + + [typeof(Short2)] = CompositePixelRegistration.Create(TextureFormat.RG16Sint), + [typeof(Short4)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Sint), + + [typeof(Rgba1010102)] = CompositePixelRegistration.Create(TextureFormat.Rgb10A2Unorm), + [typeof(Rgba32)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Unorm), + [typeof(Bgra32)] = CompositePixelRegistration.Create(TextureFormat.Bgra8Unorm), + [typeof(RgbaVector)] = CompositePixelRegistration.Create(TextureFormat.Rgba32float), + + [typeof(L16)] = CompositePixelRegistration.Create(TextureFormat.R16Uint), + [typeof(La32)] = CompositePixelRegistration.Create(TextureFormat.RG16Uint), + [typeof(Rg32)] = CompositePixelRegistration.Create(TextureFormat.RG16Uint), + [typeof(Rgba64)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Uint) + }; + + private readonly struct CompositePixelRegistration + { + public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, int pixelSizeInBytes) + { + this.PixelType = pixelType; + this.TextureFormat = textureFormat; + this.PixelSizeInBytes = pixelSizeInBytes; + } + + public Type PixelType { get; } + + public TextureFormat TextureFormat { get; } + + public int PixelSizeInBytes { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompositePixelRegistration Create(TextureFormat textureFormat) + where TPixel : unmanaged, IPixel + => new(typeof(TPixel), textureFormat, Unsafe.SizeOf()); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 01298109f..c68cd937f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// output remains deterministic and API semantics stay consistent. /// /// -internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable +internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; private const uint CoverageCoverVertexCount = 3; @@ -116,7 +116,7 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private Queue* queue; private BindGroupLayout* compositeBindGroupLayout; private PipelineLayout* compositePipelineLayout; - private RenderPipeline* compositePipeline; + private readonly ConcurrentDictionary compositePipelines = new(); private PipelineLayout* coveragePipelineLayout; private RenderPipeline* coverageStencilEvenOddPipeline; private RenderPipeline* coverageStencilNonZeroIncrementPipeline; @@ -126,7 +126,9 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private int compositeSessionDepth; private bool compositeSessionGpuActive; private bool compositeSessionDirty; - private Buffer2DRegion compositeSessionTarget; + private Rectangle compositeSessionTargetRectangle; + private int compositeSessionTargetWidth; + private int compositeSessionTargetHeight; private Texture* compositeSessionTargetTexture; private TextureView* compositeSessionTargetView; private WgpuBuffer* compositeSessionReadbackBuffer; @@ -135,6 +137,8 @@ internal sealed unsafe class WebGPUDrawingBackend : IDrawingBackend, IDisposable private ulong compositeSessionReadbackByteCount; private int compositeSessionResourceWidth; private int compositeSessionResourceHeight; + private TextureFormat compositeSessionResourceTextureFormat; + private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); private static readonly bool TraceEnabled = string.Equals( Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), "1", @@ -230,16 +234,18 @@ public void BeginCompositeSession(Configuration configuration, Buffer2DR this.compositeSessionGpuActive = false; this.compositeSessionDirty = false; - if (!CanUseGpuSession() || !this.TryEnsureGpuReady()) + if (!TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler) || + !this.TryEnsureGpuReady() || + !this.HasCompositePipelineForTextureFormat(pixelHandler.TextureFormat)) { return; } - Buffer2DRegion rgbaTarget = Unsafe.As, Buffer2DRegion>(ref target); - lock (this.gpuSync) { - if (!this.TryBeginCompositeSessionLocked(rgbaTarget)) + bool started = this.TryBeginCompositeSessionCoreLocked(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); + + if (!started) { return; } @@ -279,7 +285,7 @@ public void EndCompositeSession(Configuration configuration, Buffer2DReg Trace($"EndCompositeSession: gpuActive={this.compositeSessionGpuActive} dirty={this.compositeSessionDirty}"); if (this.compositeSessionGpuActive && this.compositeSessionDirty) { - this.TryFlushCompositeSessionLocked(); + this.TryFlushCompositeSessionLocked(target); } this.ResetCompositeSessionStateLocked(); @@ -475,9 +481,15 @@ public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions { Guard.NotNull(brush, nameof(brush)); + if (!TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler)) + { + return false; + } + return CanUseGpuComposite(graphicsOptions) && WebGpuBrushData.TryCreate(brush, out _) - && this.TryEnsureGpuReady(); + && this.TryEnsureGpuReady() + && this.HasCompositePipelineForTextureFormat(pixelHandler.TextureFormat); } /// @@ -666,9 +678,8 @@ public void CompositeCoverage( "Accelerated coverage composition requires an active composite session."); } - Buffer2DRegion rgbaTarget = Unsafe.As, Buffer2DRegion>(ref target); if (!this.TryCompositeCoverageGpu( - rgbaTarget, + target, coverageHandle, sourceOffset, brushData, @@ -722,11 +733,6 @@ public void Dispose() Trace("Dispose: begin"); lock (this.gpuSync) { - if (this.compositeSessionGpuActive && this.compositeSessionDirty) - { - this.TryFlushCompositeSessionLocked(); - } - this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); @@ -752,7 +758,7 @@ public void Dispose() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanUseGpuComposite(in GraphicsOptions graphicsOptions) where TPixel : unmanaged, IPixel - => typeof(TPixel) == typeof(Rgba32) + => HasCompositePixelHandler() && graphicsOptions.AlphaCompositionMode == PixelAlphaCompositionMode.SrcOver && graphicsOptions.ColorBlendingMode == PixelColorBlendingMode.Normal && graphicsOptions.BlendPercentage > 0F; @@ -760,7 +766,31 @@ private static bool CanUseGpuComposite(in GraphicsOptions graphicsOption [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanUseGpuSession() where TPixel : unmanaged, IPixel - => typeof(TPixel) == typeof(Rgba32); + => HasCompositePixelHandler(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasCompositePixelHandler() + where TPixel : unmanaged, IPixel + => TryGetCompositePixelHandler(out _); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler) + where TPixel : unmanaged, IPixel + => CompositePixelHandlers.TryGetValue(typeof(TPixel), out pixelHandler); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasCompositePipelineForTextureFormat(TextureFormat textureFormat) + { + if (textureFormat == TextureFormat.Undefined) + { + return false; + } + + lock (this.gpuSync) + { + return this.TryGetOrCreateCompositePipelineLocked(textureFormat, out _); + } + } /// /// Ensures this instance has a ready-to-use GPU device/pipeline set. @@ -867,8 +897,7 @@ private bool TryInitializeGpuLocked() finally { if (!this.IsGpuReady && - (this.compositePipeline is null || - this.compositePipelineLayout is null || + (this.compositePipelineLayout is null || this.compositeBindGroupLayout is null || this.coverageStencilEvenOddPipeline is null || this.coverageStencilNonZeroIncrementPipeline is null || @@ -1032,6 +1061,59 @@ private bool TryCreateCompositePipelineLocked() return false; } + // Validate that at least the baseline RGBA target format can create a pipeline. + if (!this.TryGetOrCreateCompositePipelineLocked(TextureFormat.Rgba8Unorm, out _)) + { + return false; + } + + // BGRA is optional and can fail on specific adapters/drivers. + _ = this.TryGetOrCreateCompositePipelineLocked(TextureFormat.Bgra8Unorm, out _); + return true; + } + + private bool TryGetOrCreateCompositePipelineLocked(TextureFormat textureFormat, out RenderPipeline* pipeline) + { + pipeline = null; + if (textureFormat == TextureFormat.Undefined || + this.webGpu is null || + this.device is null || + this.compositePipelineLayout is null) + { + return false; + } + + if (this.compositePipelines.TryGetValue(textureFormat, out nint existingPipelineHandle) && + existingPipelineHandle != 0) + { + pipeline = (RenderPipeline*)existingPipelineHandle; + return true; + } + + RenderPipeline* createdPipeline = this.CreateCompositePipelineForFormatLocked(textureFormat); + if (createdPipeline is null) + { + return false; + } + + nint createdPipelineHandle = (nint)createdPipeline; + nint cachedPipelineHandle = this.compositePipelines.GetOrAdd(textureFormat, createdPipelineHandle); + if (cachedPipelineHandle != createdPipelineHandle) + { + this.webGpu.RenderPipelineRelease(createdPipeline); + } + + pipeline = (RenderPipeline*)cachedPipelineHandle; + return pipeline is not null; + } + + private RenderPipeline* CreateCompositePipelineForFormatLocked(TextureFormat textureFormat) + { + if (this.webGpu is null || this.device is null) + { + return null; + } + ShaderModule* shaderModule = null; try { @@ -1057,7 +1139,7 @@ private bool TryCreateCompositePipelineLocked() if (shaderModule is null) { - return false; + return null; } ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; @@ -1066,72 +1148,13 @@ private bool TryCreateCompositePipelineLocked() { fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) { - VertexState vertexState = new() - { - Module = shaderModule, - EntryPoint = vertexEntryPointPtr, - BufferCount = 0, - Buffers = null - }; - - BlendState blendState = new() - { - Color = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - }, - Alpha = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - } - }; - - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; - colorTargets[0] = new ColorTargetState - { - Format = TextureFormat.Rgba8Unorm, - Blend = &blendState, - WriteMask = ColorWriteMask.All - }; - - FragmentState fragmentState = new() - { - Module = shaderModule, - EntryPoint = fragmentEntryPointPtr, - TargetCount = 1, - Targets = colorTargets - }; - - RenderPipelineDescriptor pipelineDescriptor = new() - { - Layout = this.compositePipelineLayout, - Vertex = vertexState, - Primitive = new PrimitiveState - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }, - DepthStencil = null, - Multisample = new MultisampleState - { - Count = 1, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }, - Fragment = &fragmentState - }; - - this.compositePipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + return this.CreateCompositePipelineLocked( + shaderModule, + vertexEntryPointPtr, + fragmentEntryPointPtr, + textureFormat); } } - - return this.compositePipeline is not null; } finally { @@ -1142,6 +1165,81 @@ private bool TryCreateCompositePipelineLocked() } } + private RenderPipeline* CreateCompositePipelineLocked( + ShaderModule* shaderModule, + byte* vertexEntryPointPtr, + byte* fragmentEntryPointPtr, + TextureFormat textureFormat) + { + if (this.webGpu is null || this.device is null || this.compositePipelineLayout is null) + { + return null; + } + + VertexState vertexState = new() + { + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + BlendState blendState = new() + { + Color = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + }, + Alpha = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + } + }; + + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = textureFormat, + Blend = &blendState, + WriteMask = ColorWriteMask.All + }; + + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; + + RenderPipelineDescriptor pipelineDescriptor = new() + { + Layout = this.compositePipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState + }; + + return this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + } + /// /// Creates the render pipeline used for coverage rasterization. /// @@ -1826,13 +1924,11 @@ private static void AddCoverageSegment( [MethodImpl(MethodImplOptions.AggressiveInlining)] private static StencilVertex ToStencilVertex(float x, float y, float widthScale, float heightScale) - { - return new StencilVertex + => new() { X = (x * widthScale) - 1F, Y = 1F - (y * heightScale) }; - } private bool WaitForSignalLocked(ManualResetEventSlim signal) { @@ -1872,14 +1968,15 @@ private bool WaitForSignalLocked(ManualResetEventSlim signal) return true; } - private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) + private bool TryQueueWriteTextureFromRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) + where TPixel : unmanaged { if (this.webGpu is null || this.queue is null || destinationTexture is null) { return false; } - int pixelSizeInBytes = Unsafe.SizeOf(); + int pixelSizeInBytes = Unsafe.SizeOf(); ImageCopyTexture destination = new() { Texture = destinationTexture, @@ -1907,8 +2004,8 @@ private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTextur RowsPerImage = (uint)sourceRegion.Height }; - Span firstRow = sourceRegion.DangerousGetRowSpan(0); - fixed (Rgba32* uploadPtr = firstRow) + Span firstRow = sourceRegion.DangerousGetRowSpan(0); + fixed (TPixel* uploadPtr = firstRow) { this.webGpu.QueueWriteTexture(this.queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); } @@ -1924,7 +2021,7 @@ private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTextur Span packedData = rented.AsSpan(0, packedByteCount); for (int y = 0; y < sourceRegion.Height; y++) { - ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); } @@ -1955,40 +2052,52 @@ private bool TryQueueWriteTextureFromRgbaRegionLocked(Texture* destinationTextur /// /// Ensures session resources for the target size, then uploads target pixels once. /// - private bool TryBeginCompositeSessionLocked(Buffer2DRegion target) + private bool TryBeginCompositeSessionCoreLocked( + Buffer2DRegion target, + TextureFormat textureFormat, + int pixelSizeInBytes) + where TPixel : unmanaged { if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || + pixelSizeInBytes <= 0 || target.Width <= 0 || target.Height <= 0) { return false; } - if (!this.TryEnsureCompositeSessionResourcesLocked(target.Width, target.Height) || + if (!this.TryEnsureCompositeSessionResourcesLocked(target.Width, target.Height, textureFormat, pixelSizeInBytes) || this.compositeSessionTargetTexture is null) { return false; } this.ResetCompositeSessionStateLocked(); - if (!this.TryQueueWriteTextureFromRgbaRegionLocked(this.compositeSessionTargetTexture, target)) + if (!this.TryQueueWriteTextureFromRegionLocked(this.compositeSessionTargetTexture, target)) { return false; } - this.compositeSessionTarget = target; + this.compositeSessionTargetRectangle = target.Rectangle; + this.compositeSessionTargetWidth = target.Width; + this.compositeSessionTargetHeight = target.Height; this.compositeSessionDirty = false; return true; } - private bool TryEnsureCompositeSessionResourcesLocked(int width, int height) + private bool TryEnsureCompositeSessionResourcesLocked( + int width, + int height, + TextureFormat textureFormat, + int pixelSizeInBytes) { if (!this.IsGpuReady || this.webGpu is null || this.device is null || + pixelSizeInBytes <= 0 || width <= 0 || height <= 0) { @@ -1999,14 +2108,15 @@ this.device is null || this.compositeSessionTargetView is not null && this.compositeSessionReadbackBuffer is not null && this.compositeSessionResourceWidth == width && - this.compositeSessionResourceHeight == height) + this.compositeSessionResourceHeight == height && + this.compositeSessionResourceTextureFormat == textureFormat) { return true; } this.ReleaseCompositeSessionResourcesLocked(); - uint textureRowBytes = checked((uint)width * (uint)Unsafe.SizeOf()); + uint textureRowBytes = checked((uint)width * (uint)pixelSizeInBytes); uint readbackRowBytes = AlignTo256(textureRowBytes); ulong readbackByteCount = (ulong)readbackRowBytes * (uint)height; @@ -2015,7 +2125,7 @@ this.compositeSessionReadbackBuffer is not null && Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, Dimension = TextureDimension.Dimension2D, Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.Rgba8Unorm, + Format = textureFormat, MipLevelCount = 1, SampleCount = 1 }; @@ -2028,7 +2138,7 @@ this.compositeSessionReadbackBuffer is not null && TextureViewDescriptor targetViewDescriptor = new() { - Format = TextureFormat.Rgba8Unorm, + Format = textureFormat, Dimension = TextureViewDimension.Dimension2D, BaseMipLevel = 0, MipLevelCount = 1, @@ -2065,13 +2175,15 @@ this.compositeSessionReadbackBuffer is not null && this.compositeSessionReadbackByteCount = readbackByteCount; this.compositeSessionResourceWidth = width; this.compositeSessionResourceHeight = height; + this.compositeSessionResourceTextureFormat = textureFormat; return true; } /// /// Reads the session target texture back into the canvas region. /// - private bool TryFlushCompositeSessionLocked() + private bool TryFlushCompositeSessionLocked(Buffer2DRegion target) + where TPixel : unmanaged, IPixel { Trace("TryFlushCompositeSessionLocked: begin"); if (this.webGpu is null || @@ -2079,14 +2191,19 @@ this.device is null || this.queue is null || this.compositeSessionTargetTexture is null || this.compositeSessionReadbackBuffer is null || - this.compositeSessionTarget.Width <= 0 || - this.compositeSessionTarget.Height <= 0 || + this.compositeSessionTargetWidth <= 0 || + this.compositeSessionTargetHeight <= 0 || this.compositeSessionReadbackByteCount == 0 || this.compositeSessionReadbackBytesPerRow == 0) { return false; } + if (target.Width != this.compositeSessionTargetWidth || target.Height != this.compositeSessionTargetHeight) + { + return false; + } + CommandEncoder* commandEncoder = this.compositeSessionCommandEncoder; bool usingSessionCommandEncoder = commandEncoder is not null; CommandBuffer* commandBuffer = null; @@ -2117,11 +2234,11 @@ this.compositeSessionReadbackBuffer is null || { Offset = 0, BytesPerRow = this.compositeSessionReadbackBytesPerRow, - RowsPerImage = (uint)this.compositeSessionTarget.Height + RowsPerImage = (uint)this.compositeSessionTargetHeight } }; - Extent3D copySize = new((uint)this.compositeSessionTarget.Width, (uint)this.compositeSessionTarget.Height, 1); + Extent3D copySize = new((uint)this.compositeSessionTargetWidth, (uint)this.compositeSessionTargetHeight, 1); this.webGpu.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); CommandBufferDescriptor commandBufferDescriptor = default; @@ -2137,10 +2254,12 @@ this.compositeSessionReadbackBuffer is null || this.webGpu.CommandBufferRelease(commandBuffer); commandBuffer = null; - if (!this.TryReadBackBufferToRgbaRegionLocked( + bool readbackSuccess = this.TryReadBackBufferToRegionLocked( this.compositeSessionReadbackBuffer, checked((int)this.compositeSessionReadbackBytesPerRow), - this.compositeSessionTarget)) + target); + + if (!readbackSuccess) { Trace("TryFlushCompositeSessionLocked: readback failed"); return false; @@ -2176,7 +2295,9 @@ private void ResetCompositeSessionStateLocked() this.compositeSessionCommandEncoder = null; } - this.compositeSessionTarget = default; + this.compositeSessionTargetRectangle = default; + this.compositeSessionTargetWidth = 0; + this.compositeSessionTargetHeight = 0; this.compositeSessionDirty = false; } @@ -2198,14 +2319,16 @@ private void ReleaseCompositeSessionResourcesLocked() this.compositeSessionReadbackByteCount = 0; this.compositeSessionResourceWidth = 0; this.compositeSessionResourceHeight = 0; + this.compositeSessionResourceTextureFormat = TextureFormat.Undefined; } - private bool TryCompositeCoverageGpu( - Buffer2DRegion target, + private bool TryCompositeCoverageGpu( + Buffer2DRegion target, DrawingCoverageHandle coverageHandle, Point sourceOffset, WebGpuBrushData brushData, float blendPercentage) + where TPixel : unmanaged, IPixel { if (!coverageHandle.IsValid) { @@ -2239,12 +2362,12 @@ private bool TryCompositeCoverageGpu( return true; } - Buffer2DRegion destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); + Buffer2DRegion destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); lock (this.gpuSync) { if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || - this.compositePipeline is null || this.compositeBindGroupLayout is null) + this.compositeBindGroupLayout is null) { return false; } @@ -2258,16 +2381,22 @@ private bool TryCompositeCoverageGpu( this.compositeSessionTargetTexture is not null && this.compositeSessionTargetView is not null) { - int destinationX = destinationRegion.Rectangle.X - this.compositeSessionTarget.Rectangle.X; - int destinationY = destinationRegion.Rectangle.Y - this.compositeSessionTarget.Rectangle.Y; - if ((uint)destinationX >= (uint)this.compositeSessionTarget.Width || - (uint)destinationY >= (uint)this.compositeSessionTarget.Height) + RenderPipeline* compositePipeline = this.GetCompositeSessionPipelineLocked(); + if (compositePipeline is null) { return false; } - int sessionCompositeWidth = Math.Min(compositeWidth, this.compositeSessionTarget.Width - destinationX); - int sessionCompositeHeight = Math.Min(compositeHeight, this.compositeSessionTarget.Height - destinationY); + int destinationX = destinationRegion.Rectangle.X - this.compositeSessionTargetRectangle.X; + int destinationY = destinationRegion.Rectangle.Y - this.compositeSessionTargetRectangle.Y; + if ((uint)destinationX >= (uint)this.compositeSessionTargetWidth || + (uint)destinationY >= (uint)this.compositeSessionTargetHeight) + { + return false; + } + + int sessionCompositeWidth = Math.Min(compositeWidth, this.compositeSessionTargetWidth - destinationX); + int sessionCompositeHeight = Math.Min(compositeHeight, this.compositeSessionTargetHeight - destinationY); if (sessionCompositeWidth <= 0 || sessionCompositeHeight <= 0) { return true; @@ -2280,13 +2409,14 @@ this.compositeSessionTargetTexture is not null && if (this.TryRunCompositePassLocked( this.compositeSessionCommandEncoder, + compositePipeline, entry, sourceOffset, brushData, blendPercentage, this.compositeSessionTargetView, - this.compositeSessionTarget.Width, - this.compositeSessionTarget.Height, + this.compositeSessionTargetWidth, + this.compositeSessionTargetHeight, destinationX, destinationY, sessionCompositeWidth, @@ -2296,11 +2426,6 @@ this.compositeSessionTargetTexture is not null && return true; } - if (this.compositeSessionDirty) - { - this.TryFlushCompositeSessionLocked(); - } - this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); this.compositeSessionGpuActive = false; @@ -2328,6 +2453,19 @@ private bool TryEnsureCompositeSessionCommandEncoderLocked() return this.compositeSessionCommandEncoder is not null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private RenderPipeline* GetCompositeSessionPipelineLocked() + { + if (this.compositeSessionResourceTextureFormat == TextureFormat.Undefined) + { + return null; + } + + return this.TryGetOrCreateCompositePipelineLocked(this.compositeSessionResourceTextureFormat, out RenderPipeline* pipeline) + ? pipeline + : null; + } + private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) { if (entry.GpuCoverageTexture is not null && entry.GpuCoverageView is not null) @@ -2343,6 +2481,7 @@ private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) /// private bool TryRunCompositePassLocked( CommandEncoder* commandEncoder, + RenderPipeline* compositePipeline, CoverageEntry coverageEntry, Point sourceOffset, WebGpuBrushData brushData, @@ -2358,7 +2497,7 @@ private bool TryRunCompositePassLocked( if (this.webGpu is null || this.device is null || this.queue is null || - this.compositePipeline is null || + compositePipeline is null || this.compositeBindGroupLayout is null || coverageEntry.GpuCoverageView is null || targetView is null || @@ -2474,7 +2613,7 @@ targetView is null || return false; } - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.compositePipeline); + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, compositePipeline); this.webGpu.RenderPassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, (uint*)null); this.webGpu.RenderPassEncoderDraw(passEncoder, CompositeVertexCount, 1, 0, 0); this.webGpu.RenderPassEncoderEnd(passEncoder); @@ -2566,17 +2705,18 @@ void Callback(BufferMapAsyncStatus status, void* userDataPtr) return true; } - private bool TryReadBackBufferToRgbaRegionLocked( + private bool TryReadBackBufferToRegionLocked( WgpuBuffer* readbackBuffer, int sourceRowBytes, - Buffer2DRegion destinationRegion) + Buffer2DRegion destinationRegion) + where TPixel : unmanaged { if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) { return true; } - int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); + int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); int readbackByteCount = checked(sourceRowBytes * destinationRegion.Height); if (!this.TryMapReadBufferLocked(readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) { @@ -2586,13 +2726,13 @@ private bool TryReadBackBufferToRgbaRegionLocked( try { ReadOnlySpan sourceData = new(mappedData, readbackByteCount); - int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); + int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); // If the target region spans full rows in a contiguous backing buffer we can copy // the mapped data in one block instead of per-row. if (destinationRegion.Rectangle.X == 0 && sourceRowBytes == destinationStrideBytes && - TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) + TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) { Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); int destinationStart = checked(destinationRegion.Rectangle.Y * destinationStrideBytes); @@ -2607,7 +2747,7 @@ private bool TryReadBackBufferToRgbaRegionLocked( for (int y = 0; y < destinationRegion.Height; y++) { ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); - MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); + MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); } return true; @@ -2743,12 +2883,16 @@ private void ReleaseGpuResourcesLocked() this.coveragePipelineLayout = null; } - if (this.compositePipeline is not null) + foreach (KeyValuePair compositePipelineEntry in this.compositePipelines) { - this.webGpu.RenderPipelineRelease(this.compositePipeline); - this.compositePipeline = null; + if (compositePipelineEntry.Value != 0) + { + this.webGpu.RenderPipelineRelease((RenderPipeline*)compositePipelineEntry.Value); + } } + this.compositePipelines.Clear(); + if (this.compositePipelineLayout is not null) { this.webGpu.PipelineLayoutRelease(this.compositePipelineLayout); @@ -2857,9 +3001,7 @@ public CoverageSegment(float fromX, float fromY, float toX, float toY) private readonly struct CoverageTriangleData { public CoverageTriangleData(StencilVertex[] vertices) - { - this.Vertices = vertices; - } + => this.Vertices = vertices; public StencilVertex[] Vertices { get; } } From 139cbb452259af3cd9e6367dd1d22353cd688bcf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 21 Feb 2026 14:16:39 +1000 Subject: [PATCH 005/136] WebGPU: coverage scratch & dynamic uniform buffers --- .../WebGPUDrawingBackend.cs | 736 ++++++++++++------ 1 file changed, 506 insertions(+), 230 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index c68cd937f..583eb4615 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -85,6 +85,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; + private const uint CompositeUniformAlignment = 256; + private const uint CompositeUniformBufferSize = 256 * 1024; private const uint CoverageCoverVertexCount = 3; private const uint CoverageSampleCount = 4; private const int CallbackTimeoutMilliseconds = 10_000; @@ -126,18 +128,29 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private int compositeSessionDepth; private bool compositeSessionGpuActive; private bool compositeSessionDirty; + private RenderPassEncoder* compositeSessionPassEncoder; private Rectangle compositeSessionTargetRectangle; private int compositeSessionTargetWidth; private int compositeSessionTargetHeight; private Texture* compositeSessionTargetTexture; private TextureView* compositeSessionTargetView; private WgpuBuffer* compositeSessionReadbackBuffer; + private WgpuBuffer* compositeSessionUniformBuffer; + private uint compositeSessionUniformWriteOffset; private CommandEncoder* compositeSessionCommandEncoder; private uint compositeSessionReadbackBytesPerRow; private ulong compositeSessionReadbackByteCount; private int compositeSessionResourceWidth; private int compositeSessionResourceHeight; private TextureFormat compositeSessionResourceTextureFormat; + private Texture* coverageScratchMultisampleTexture; + private TextureView* coverageScratchMultisampleView; + private Texture* coverageScratchStencilTexture; + private TextureView* coverageScratchStencilView; + private int coverageScratchWidth; + private int coverageScratchHeight; + private WgpuBuffer* coverageScratchVertexBuffer; + private ulong coverageScratchVertexCapacityBytes; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); private static readonly bool TraceEnabled = string.Equals( Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), @@ -1031,6 +1044,7 @@ private bool TryCreateCompositePipelineLocked() Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, + HasDynamicOffset = true, MinBindingSize = (ulong)Unsafe.SizeOf() } }; @@ -1414,14 +1428,11 @@ private bool TryCreateCoveragePipelineLocked() DepthBiasClamp = 0F }; - PrimitiveState incrementPrimitiveState = primitiveState; - incrementPrimitiveState.CullMode = CullMode.Back; - RenderPipelineDescriptor incrementPipelineDescriptor = new() { Layout = this.coveragePipelineLayout, Vertex = stencilVertexState, - Primitive = incrementPrimitiveState, + Primitive = primitiveState, DepthStencil = &incrementDepthStencilState, Multisample = multisampleState, Fragment = &stencilFragmentState @@ -1455,14 +1466,11 @@ private bool TryCreateCoveragePipelineLocked() DepthBiasClamp = 0F }; - PrimitiveState decrementPrimitiveState = primitiveState; - decrementPrimitiveState.CullMode = CullMode.Front; - RenderPipelineDescriptor decrementPipelineDescriptor = new() { Layout = this.coveragePipelineLayout, Vertex = stencilVertexState, - Primitive = decrementPrimitiveState, + Primitive = primitiveState, DepthStencil = &decrementDepthStencilState, Multisample = multisampleState, Fragment = &stencilFragmentState @@ -1562,6 +1570,160 @@ private bool TryCreateCoveragePipelineLocked() } } + private bool TryEnsureCoverageScratchTargetsLocked( + int width, + int height, + out TextureView* multisampleCoverageView, + out TextureView* stencilView) + { + multisampleCoverageView = null; + stencilView = null; + + if (this.webGpu is null || this.device is null || width <= 0 || height <= 0) + { + return false; + } + + if (this.coverageScratchMultisampleView is not null && + this.coverageScratchStencilView is not null && + this.coverageScratchWidth == width && + this.coverageScratchHeight == height) + { + multisampleCoverageView = this.coverageScratchMultisampleView; + stencilView = this.coverageScratchStencilView; + return true; + } + + this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); + this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); + this.ReleaseTextureViewLocked(this.coverageScratchStencilView); + this.ReleaseTextureLocked(this.coverageScratchStencilTexture); + this.coverageScratchMultisampleView = null; + this.coverageScratchMultisampleTexture = null; + this.coverageScratchStencilView = null; + this.coverageScratchStencilTexture = null; + this.coverageScratchWidth = 0; + this.coverageScratchHeight = 0; + + TextureDescriptor multisampleCoverageTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = CoverageSampleCount + }; + + Texture* createdMultisampleCoverageTexture = + this.webGpu.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); + if (createdMultisampleCoverageTexture is null) + { + return false; + } + + TextureViewDescriptor coverageViewDescriptor = new() + { + Format = TextureFormat.R8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* createdMultisampleCoverageView = this.webGpu.TextureCreateView(createdMultisampleCoverageTexture, in coverageViewDescriptor); + if (createdMultisampleCoverageView is null) + { + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + TextureDescriptor stencilTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = TextureFormat.Depth24PlusStencil8, + MipLevelCount = 1, + SampleCount = CoverageSampleCount + }; + + Texture* createdStencilTexture = this.webGpu.DeviceCreateTexture(this.device, in stencilTextureDescriptor); + if (createdStencilTexture is null) + { + this.ReleaseTextureViewLocked(createdMultisampleCoverageView); + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + TextureViewDescriptor stencilViewDescriptor = new() + { + Format = TextureFormat.Depth24PlusStencil8, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* createdStencilView = this.webGpu.TextureCreateView(createdStencilTexture, in stencilViewDescriptor); + if (createdStencilView is null) + { + this.ReleaseTextureLocked(createdStencilTexture); + this.ReleaseTextureViewLocked(createdMultisampleCoverageView); + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + this.coverageScratchMultisampleTexture = createdMultisampleCoverageTexture; + this.coverageScratchMultisampleView = createdMultisampleCoverageView; + this.coverageScratchStencilTexture = createdStencilTexture; + this.coverageScratchStencilView = createdStencilView; + this.coverageScratchWidth = width; + this.coverageScratchHeight = height; + + multisampleCoverageView = createdMultisampleCoverageView; + stencilView = createdStencilView; + return true; + } + + private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) + { + if (this.webGpu is null || this.device is null || requiredByteCount == 0) + { + return false; + } + + if (this.coverageScratchVertexBuffer is not null && + this.coverageScratchVertexCapacityBytes >= requiredByteCount) + { + return true; + } + + this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); + this.coverageScratchVertexBuffer = null; + this.coverageScratchVertexCapacityBytes = 0; + + BufferDescriptor vertexBufferDescriptor = new() + { + Usage = BufferUsage.Vertex | BufferUsage.CopyDst, + Size = requiredByteCount + }; + + WgpuBuffer* createdVertexBuffer = this.webGpu.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); + if (createdVertexBuffer is null) + { + return false; + } + + this.coverageScratchVertexBuffer = createdVertexBuffer; + this.coverageScratchVertexCapacityBytes = requiredByteCount; + return true; + } + /// /// Rasterizes edge triangles through a stencil-and-cover pass into an R8Unorm texture. /// @@ -1571,7 +1733,7 @@ private bool TryRasterizeCoverageTextureLocked( out Texture* coverageTexture, out TextureView* coverageView) { - Trace($"TryRasterizeCoverageTextureLocked: begin triangles={coverageTriangleData.Vertices.Length / 3} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); + Trace($"TryRasterizeCoverageTextureLocked: begin triangles={coverageTriangleData.TotalVertexCount / 3} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); coverageTexture = null; coverageView = null; @@ -1582,7 +1744,7 @@ this.coverageStencilEvenOddPipeline is null || this.coverageStencilNonZeroIncrementPipeline is null || this.coverageStencilNonZeroDecrementPipeline is null || this.coverageCoverPipeline is null || - coverageTriangleData.Vertices.Length == 0 || + coverageTriangleData.TotalVertexCount == 0 || rasterizerOptions.Interest.Width <= 0 || rasterizerOptions.Interest.Height <= 0) { @@ -1591,17 +1753,21 @@ this.coverageCoverPipeline is null || Texture* createdCoverageTexture = null; TextureView* createdCoverageView = null; - Texture* multisampleCoverageTexture = null; - TextureView* multisampleCoverageView = null; - Texture* stencilTexture = null; - TextureView* stencilView = null; - WgpuBuffer* vertexBuffer = null; CommandEncoder* commandEncoder = null; RenderPassEncoder* passEncoder = null; CommandBuffer* commandBuffer = null; bool success = false; try { + if (!this.TryEnsureCoverageScratchTargetsLocked( + rasterizerOptions.Interest.Width, + rasterizerOptions.Interest.Height, + out TextureView* multisampleCoverageView, + out TextureView* stencilView)) + { + return false; + } + TextureDescriptor coverageTextureDescriptor = new() { Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, @@ -1635,76 +1801,15 @@ this.coverageCoverPipeline is null || return false; } - TextureDescriptor multisampleCoverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - multisampleCoverageTexture = this.webGpu.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); - if (multisampleCoverageTexture is null) - { - return false; - } - - multisampleCoverageView = this.webGpu.TextureCreateView(multisampleCoverageTexture, in coverageViewDescriptor); - if (multisampleCoverageView is null) - { - return false; - } - - TextureDescriptor stencilTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), - Format = TextureFormat.Depth24PlusStencil8, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - stencilTexture = this.webGpu.DeviceCreateTexture(this.device, in stencilTextureDescriptor); - if (stencilTexture is null) - { - return false; - } - - TextureViewDescriptor stencilViewDescriptor = new() - { - Format = TextureFormat.Depth24PlusStencil8, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - stencilView = this.webGpu.TextureCreateView(stencilTexture, in stencilViewDescriptor); - if (stencilView is null) - { - return false; - } - - ulong vertexByteCount = checked((ulong)coverageTriangleData.Vertices.Length * (ulong)Unsafe.SizeOf()); - BufferDescriptor vertexBufferDescriptor = new() - { - Usage = BufferUsage.Vertex | BufferUsage.CopyDst, - Size = vertexByteCount - }; - vertexBuffer = this.webGpu.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); - if (vertexBuffer is null) + ulong vertexByteCount = checked((ulong)coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); + if (!this.TryEnsureCoverageScratchVertexBufferLocked(vertexByteCount) || this.coverageScratchVertexBuffer is null) { return false; } fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) { - this.webGpu.QueueWriteBuffer(this.queue, vertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); + this.webGpu.QueueWriteBuffer(this.queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); } CommandEncoderDescriptor commandEncoderDescriptor = default; @@ -1750,19 +1855,30 @@ this.coverageCoverPipeline is null || } this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); - this.webGpu.RenderPassEncoderSetVertexBuffer(passEncoder, 0, vertexBuffer, 0, vertexByteCount); + this.webGpu.RenderPassEncoderSetVertexBuffer(passEncoder, 0, this.coverageScratchVertexBuffer, 0, vertexByteCount); if (rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd) { this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilEvenOddPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + this.webGpu.RenderPassEncoderDraw(passEncoder, coverageTriangleData.TotalVertexCount, 1, 0, 0); } else { - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + if (coverageTriangleData.IncrementVertexCount > 0) + { + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); + this.webGpu.RenderPassEncoderDraw(passEncoder, coverageTriangleData.IncrementVertexCount, 1, 0, 0); + } - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, (uint)coverageTriangleData.Vertices.Length, 1, 0, 0); + if (coverageTriangleData.DecrementVertexCount > 0) + { + this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); + this.webGpu.RenderPassEncoderDraw( + passEncoder, + coverageTriangleData.DecrementVertexCount, + 1, + coverageTriangleData.IncrementVertexCount, + 0); + } } this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); @@ -1809,12 +1925,6 @@ this.coverageCoverPipeline is null || this.webGpu.CommandEncoderRelease(commandEncoder); } - this.ReleaseBufferLocked(vertexBuffer); - this.ReleaseTextureViewLocked(stencilView); - this.ReleaseTextureLocked(stencilTexture); - this.ReleaseTextureViewLocked(multisampleCoverageView); - this.ReleaseTextureLocked(multisampleCoverageTexture); - if (!success) { this.ReleaseTextureViewLocked(createdCoverageView); @@ -1824,8 +1934,8 @@ this.coverageCoverPipeline is null || } /// - /// Flattens a path into local-interest coordinates and converts each edge to a triangle - /// anchored at an external origin. These triangles are consumed by the stencil pass. + /// Flattens a path into local-interest coordinates and converts each non-horizontal edge + /// into a trapezoid (two triangles) anchored at a left-side sentinel X. /// private static bool TryBuildCoverageTriangles( IPath path, @@ -1846,7 +1956,6 @@ private static bool TryBuildCoverageTriangles( List segments = []; float minX = float.PositiveInfinity; - float minY = float.PositiveInfinity; foreach (ISimplePath simplePath in path.Flatten()) { @@ -1858,35 +1967,97 @@ private static bool TryBuildCoverageTriangles( for (int i = 1; i < points.Length; i++) { - AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX, ref minY); + AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX); } if (simplePath.IsClosed) { - AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX, ref minY); + AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX); + } + } + + if (segments.Count == 0 || !float.IsFinite(minX)) + { + return false; + } + + int incrementEdgeCount = 0; + int decrementEdgeCount = 0; + foreach (CoverageSegment segment in segments) + { + if (segment.FromY == segment.ToY) + { + continue; + } + + if (segment.ToY > segment.FromY) + { + incrementEdgeCount++; + } + else + { + decrementEdgeCount++; } } - if (segments.Count == 0 || !float.IsFinite(minX) || !float.IsFinite(minY)) + int totalEdgeCount = incrementEdgeCount + decrementEdgeCount; + if (totalEdgeCount == 0) { return false; } - float originX = minX - 1F; - float originY = minY - 1F; + float sentinelX = minX - 1F; float widthScale = 2F / interestSize.Width; float heightScale = 2F / interestSize.Height; + int incrementVertexCount = checked(incrementEdgeCount * 6); + int decrementVertexCount = checked(decrementEdgeCount * 6); + StencilVertex[] vertices = new StencilVertex[checked(incrementVertexCount + decrementVertexCount)]; - StencilVertex[] vertices = new StencilVertex[checked(segments.Count * 3)]; int vertexIndex = 0; foreach (CoverageSegment segment in segments) { - vertices[vertexIndex++] = ToStencilVertex(originX, originY, widthScale, heightScale); - vertices[vertexIndex++] = ToStencilVertex(segment.FromX, segment.FromY, widthScale, heightScale); - vertices[vertexIndex++] = ToStencilVertex(segment.ToX, segment.ToY, widthScale, heightScale); + if (segment.ToY <= segment.FromY) + { + continue; + } + + AppendCoverageEdgeQuad( + vertices, + ref vertexIndex, + sentinelX, + segment.FromX, + segment.FromY, + segment.ToX, + segment.ToY, + widthScale, + heightScale); + } + + int decrementStartIndex = incrementVertexCount; + vertexIndex = decrementStartIndex; + foreach (CoverageSegment segment in segments) + { + if (segment.ToY >= segment.FromY) + { + continue; + } + + AppendCoverageEdgeQuad( + vertices, + ref vertexIndex, + sentinelX, + segment.FromX, + segment.FromY, + segment.ToX, + segment.ToY, + widthScale, + heightScale); } - coverageTriangleData = new CoverageTriangleData(vertices); + coverageTriangleData = new CoverageTriangleData( + vertices, + (uint)incrementVertexCount, + (uint)decrementVertexCount); return true; } @@ -1896,8 +2067,7 @@ private static void AddCoverageSegment( float offsetX, float offsetY, List destination, - ref float minX, - ref float minY) + ref float minX) { if (from.Equals(to)) { @@ -1919,7 +2089,30 @@ private static void AddCoverageSegment( destination.Add(new CoverageSegment(fromX, fromY, toX, toY)); minX = MathF.Min(minX, MathF.Min(fromX, toX)); - minY = MathF.Min(minY, MathF.Min(fromY, toY)); + } + + private static void AppendCoverageEdgeQuad( + StencilVertex[] destination, + ref int destinationIndex, + float sentinelX, + float fromX, + float fromY, + float toX, + float toY, + float widthScale, + float heightScale) + { + StencilVertex a = ToStencilVertex(sentinelX, fromY, widthScale, heightScale); + StencilVertex b = ToStencilVertex(fromX, fromY, widthScale, heightScale); + StencilVertex c = ToStencilVertex(toX, toY, widthScale, heightScale); + StencilVertex d = ToStencilVertex(sentinelX, toY, widthScale, heightScale); + + destination[destinationIndex++] = a; + destination[destinationIndex++] = b; + destination[destinationIndex++] = c; + destination[destinationIndex++] = a; + destination[destinationIndex++] = c; + destination[destinationIndex++] = d; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -2084,6 +2277,7 @@ this.queue is null || this.compositeSessionTargetRectangle = target.Rectangle; this.compositeSessionTargetWidth = target.Width; this.compositeSessionTargetHeight = target.Height; + this.compositeSessionUniformWriteOffset = 0; this.compositeSessionDirty = false; return true; } @@ -2107,6 +2301,7 @@ this.device is null || if (this.compositeSessionTargetTexture is not null && this.compositeSessionTargetView is not null && this.compositeSessionReadbackBuffer is not null && + this.compositeSessionUniformBuffer is not null && this.compositeSessionResourceWidth == width && this.compositeSessionResourceHeight == height && this.compositeSessionResourceTextureFormat == textureFormat) @@ -2168,9 +2363,26 @@ this.compositeSessionReadbackBuffer is not null && return false; } + BufferDescriptor uniformBufferDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = CompositeUniformBufferSize + }; + + WgpuBuffer* uniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); + if (uniformBuffer is null) + { + this.ReleaseBufferLocked(readbackBuffer); + this.ReleaseTextureViewLocked(targetView); + this.ReleaseTextureLocked(targetTexture); + return false; + } + this.compositeSessionTargetTexture = targetTexture; this.compositeSessionTargetView = targetView; this.compositeSessionReadbackBuffer = readbackBuffer; + this.compositeSessionUniformBuffer = uniformBuffer; + this.compositeSessionUniformWriteOffset = 0; this.compositeSessionReadbackBytesPerRow = readbackRowBytes; this.compositeSessionReadbackByteCount = readbackByteCount; this.compositeSessionResourceWidth = width; @@ -2209,6 +2421,8 @@ this.compositeSessionReadbackBuffer is null || CommandBuffer* commandBuffer = null; try { + this.TryCloseCompositeSessionPassLocked(); + if (commandEncoder is null) { CommandEncoderDescriptor commandEncoderDescriptor = default; @@ -2289,6 +2503,8 @@ this.compositeSessionReadbackBuffer is null || private void ResetCompositeSessionStateLocked() { + this.TryCloseCompositeSessionPassLocked(); + if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) { this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); @@ -2303,15 +2519,25 @@ private void ResetCompositeSessionStateLocked() private void ReleaseCompositeSessionResourcesLocked() { + if (this.compositeSessionPassEncoder is not null && this.webGpu is not null) + { + this.webGpu.RenderPassEncoderRelease(this.compositeSessionPassEncoder); + this.compositeSessionPassEncoder = null; + } + if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) { this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); this.compositeSessionCommandEncoder = null; } + this.ReleaseAllCoverageCompositeBindGroupsLocked(); + this.ReleaseBufferLocked(this.compositeSessionUniformBuffer); this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); this.ReleaseTextureViewLocked(this.compositeSessionTargetView); this.ReleaseTextureLocked(this.compositeSessionTargetTexture); + this.compositeSessionUniformBuffer = null; + this.compositeSessionUniformWriteOffset = 0; this.compositeSessionReadbackBuffer = null; this.compositeSessionTargetTexture = null; this.compositeSessionTargetView = null; @@ -2453,6 +2679,18 @@ private bool TryEnsureCompositeSessionCommandEncoderLocked() return this.compositeSessionCommandEncoder is not null; } + private void TryCloseCompositeSessionPassLocked() + { + if (this.compositeSessionPassEncoder is null || this.webGpu is null) + { + return; + } + + this.webGpu.RenderPassEncoderEnd(this.compositeSessionPassEncoder); + this.webGpu.RenderPassEncoderRelease(this.compositeSessionPassEncoder); + this.compositeSessionPassEncoder = null; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private RenderPipeline* GetCompositeSessionPipelineLocked() { @@ -2476,6 +2714,61 @@ private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) return false; } + private BindGroup* GetOrCreateCoverageBindGroupLocked( + CoverageEntry coverageEntry, + WgpuBuffer* uniformBuffer, + uint uniformDataSize) + { + if (this.webGpu is null || + this.device is null || + this.compositeBindGroupLayout is null || + coverageEntry.GpuCoverageView is null || + uniformBuffer is null || + uniformDataSize == 0) + { + return null; + } + + if (coverageEntry.GpuCompositeBindGroup is not null && + coverageEntry.GpuCompositeUniformBuffer == uniformBuffer) + { + return coverageEntry.GpuCompositeBindGroup; + } + + this.ReleaseCoverageCompositeBindGroupLocked(coverageEntry); + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageEntry.GpuCoverageView + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = uniformBuffer, + Offset = 0, + Size = uniformDataSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = this.compositeBindGroupLayout, + EntryCount = 2, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); + if (bindGroup is null) + { + return null; + } + + coverageEntry.GpuCompositeBindGroup = bindGroup; + coverageEntry.GpuCompositeUniformBuffer = uniformBuffer; + return bindGroup; + } + /// /// Executes one composition draw call into the session target texture. /// @@ -2512,86 +2805,58 @@ targetView is null || return true; } - ulong uniformByteCount = (ulong)Unsafe.SizeOf(); - WgpuBuffer* uniformBuffer = null; - BindGroup* bindGroup = null; - CommandEncoder* createdCommandEncoder = null; - RenderPassEncoder* passEncoder = null; - CommandBuffer* commandBuffer = null; - try + if (this.compositeSessionUniformBuffer is null) { - BufferDescriptor uniformBufferDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = uniformByteCount - }; - uniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); - if (uniformBuffer is null) - { - return false; - } + return false; + } - CompositeParams parameters = new() - { - SourceOffsetX = (uint)sourceOffset.X, - SourceOffsetY = (uint)sourceOffset.Y, - DestinationX = (uint)destinationX, - DestinationY = (uint)destinationY, - DestinationWidth = (uint)compositeWidth, - DestinationHeight = (uint)compositeHeight, - TargetWidth = (uint)targetWidth, - TargetHeight = (uint)targetHeight, - BrushKind = (uint)brushData.Kind, - SolidBrushColor = brushData.SolidColor, - BlendPercentage = blendPercentage - }; + uint uniformDataSize = (uint)Unsafe.SizeOf(); + uint uniformStride = AlignTo256(uniformDataSize); + if (uniformStride == 0 || + this.compositeSessionUniformWriteOffset > CompositeUniformBufferSize || + this.compositeSessionUniformWriteOffset + uniformStride > CompositeUniformBufferSize) + { + return false; + } - this.webGpu.QueueWriteBuffer( - this.queue, - uniformBuffer, - 0, - ref parameters, - (nuint)Unsafe.SizeOf()); + uint uniformOffset = this.compositeSessionUniformWriteOffset; + this.compositeSessionUniformWriteOffset += uniformStride; - BindGroupEntry* bindEntries = stackalloc BindGroupEntry[2]; - bindEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageEntry.GpuCoverageView - }; - bindEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = uniformBuffer, - Offset = 0, - Size = uniformByteCount - }; + BindGroup* bindGroup = this.GetOrCreateCoverageBindGroupLocked(coverageEntry, this.compositeSessionUniformBuffer, uniformDataSize); + if (bindGroup is null) + { + return false; + } - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = this.compositeBindGroupLayout, - EntryCount = 2, - Entries = bindEntries - }; - bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); - if (bindGroup is null) - { - return false; - } + if (commandEncoder is null) + { + return false; + } - CommandEncoder* compositeCommandEncoder = commandEncoder; - if (compositeCommandEncoder is null) - { - CommandEncoderDescriptor commandEncoderDescriptor = default; - createdCommandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); - if (createdCommandEncoder is null) - { - return false; - } + CompositeParams parameters = new() + { + SourceOffsetX = (uint)sourceOffset.X, + SourceOffsetY = (uint)sourceOffset.Y, + DestinationX = (uint)destinationX, + DestinationY = (uint)destinationY, + DestinationWidth = (uint)compositeWidth, + DestinationHeight = (uint)compositeHeight, + TargetWidth = (uint)targetWidth, + TargetHeight = (uint)targetHeight, + BrushKind = (uint)brushData.Kind, + SolidBrushColor = brushData.SolidColor, + BlendPercentage = blendPercentage + }; - compositeCommandEncoder = createdCommandEncoder; - } + this.webGpu.QueueWriteBuffer( + this.queue, + this.compositeSessionUniformBuffer, + uniformOffset, + ref parameters, + (nuint)Unsafe.SizeOf()); + if (this.compositeSessionPassEncoder is null) + { RenderPassColorAttachment colorAttachment = new() { View = targetView, @@ -2607,61 +2872,20 @@ targetView is null || ColorAttachments = &colorAttachment }; - passEncoder = this.webGpu.CommandEncoderBeginRenderPass(compositeCommandEncoder, in renderPassDescriptor); - if (passEncoder is null) - { - return false; - } - - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, compositePipeline); - this.webGpu.RenderPassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, (uint*)null); - this.webGpu.RenderPassEncoderDraw(passEncoder, CompositeVertexCount, 1, 0, 0); - this.webGpu.RenderPassEncoderEnd(passEncoder); - this.webGpu.RenderPassEncoderRelease(passEncoder); - passEncoder = null; - - if (createdCommandEncoder is null) - { - return true; - } - - CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGpu.CommandEncoderFinish(createdCommandEncoder, in commandBufferDescriptor); - if (commandBuffer is null) + this.compositeSessionPassEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + if (this.compositeSessionPassEncoder is null) { return false; } - - this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - - this.webGpu.CommandBufferRelease(commandBuffer); - commandBuffer = null; - return true; } - finally - { - if (passEncoder is not null) - { - this.webGpu.RenderPassEncoderRelease(passEncoder); - } - - if (commandBuffer is not null) - { - this.webGpu.CommandBufferRelease(commandBuffer); - } - - if (createdCommandEncoder is not null) - { - this.webGpu.CommandEncoderRelease(createdCommandEncoder); - } - if (bindGroup is not null) - { - this.webGpu.BindGroupRelease(bindGroup); - } + uint dynamicOffset = uniformOffset; + uint* dynamicOffsets = &dynamicOffset; - this.ReleaseBufferLocked(uniformBuffer); - } + this.webGpu.RenderPassEncoderSetPipeline(this.compositeSessionPassEncoder, compositePipeline); + this.webGpu.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 1, dynamicOffsets); + this.webGpu.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, 1, 0, 0); + return true; } private bool TryMapReadBufferLocked(WgpuBuffer* readbackBuffer, nuint byteCount, out byte* mappedData) @@ -2762,6 +2986,7 @@ private bool TryReadBackBufferToRegionLocked( private void ReleaseCoverageTextureLocked(CoverageEntry entry) { + this.ReleaseCoverageCompositeBindGroupLocked(entry); Trace($"ReleaseCoverageTextureLocked: tex={(nint)entry.GpuCoverageTexture:X} view={(nint)entry.GpuCoverageView:X}"); this.ReleaseTextureViewLocked(entry.GpuCoverageView); this.ReleaseTextureLocked(entry.GpuCoverageTexture); @@ -2769,6 +2994,42 @@ private void ReleaseCoverageTextureLocked(CoverageEntry entry) entry.GpuCoverageTexture = null; } + private void ReleaseCoverageCompositeBindGroupLocked(CoverageEntry entry) + { + if (entry.GpuCompositeBindGroup is not null && this.webGpu is not null) + { + this.webGpu.BindGroupRelease(entry.GpuCompositeBindGroup); + } + + entry.GpuCompositeBindGroup = null; + entry.GpuCompositeUniformBuffer = null; + } + + private void ReleaseAllCoverageCompositeBindGroupsLocked() + { + foreach (KeyValuePair kv in this.preparedCoverage) + { + this.ReleaseCoverageCompositeBindGroupLocked(kv.Value); + } + } + + private void ReleaseCoverageScratchResourcesLocked() + { + this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); + this.ReleaseTextureViewLocked(this.coverageScratchStencilView); + this.ReleaseTextureLocked(this.coverageScratchStencilTexture); + this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); + this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); + this.coverageScratchVertexBuffer = null; + this.coverageScratchVertexCapacityBytes = 0; + this.coverageScratchStencilView = null; + this.coverageScratchStencilTexture = null; + this.coverageScratchMultisampleView = null; + this.coverageScratchMultisampleTexture = null; + this.coverageScratchWidth = 0; + this.coverageScratchHeight = 0; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; @@ -2850,6 +3111,7 @@ private void ReleaseGpuResourcesLocked() Trace("ReleaseGpuResourcesLocked: begin"); this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); + this.ReleaseCoverageScratchResourcesLocked(); if (this.webGpu is not null) { @@ -3000,10 +3262,20 @@ public CoverageSegment(float fromX, float fromY, float toX, float toY) private readonly struct CoverageTriangleData { - public CoverageTriangleData(StencilVertex[] vertices) - => this.Vertices = vertices; + public CoverageTriangleData(StencilVertex[] vertices, uint incrementVertexCount, uint decrementVertexCount) + { + this.Vertices = vertices; + this.IncrementVertexCount = incrementVertexCount; + this.DecrementVertexCount = decrementVertexCount; + } public StencilVertex[] Vertices { get; } + + public uint IncrementVertexCount { get; } + + public uint DecrementVertexCount { get; } + + public uint TotalVertexCount => this.IncrementVertexCount + this.DecrementVertexCount; } private sealed class CoverageEntry : IDisposable @@ -3026,6 +3298,10 @@ public CoverageEntry(int width, int height) public TextureView* GpuCoverageView { get; set; } + public BindGroup* GpuCompositeBindGroup { get; set; } + + public WgpuBuffer* GpuCompositeUniformBuffer { get; set; } + public void Dispose() { } From 4d5ce89b0ecb30aac5894ce498f815570aaf3b64 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 21 Feb 2026 19:27:34 +1000 Subject: [PATCH 006/136] Introduce ICanvasFrame and native surface support --- .../WebGPUDrawingBackend.cs | 268 +++++++++++++++--- .../WebGpuSurfaceCapability.cs | 83 ++++++ .../Backends/CanvasRegionFrame{TPixel}.cs | 48 ++++ .../Backends/CpuCanvasFrame{TPixel}.cs | 39 +++ .../Backends/DefaultDrawingBackend.cs | 52 +++- .../Processing/Backends/ICanvasFrame.cs | 34 +++ .../Processing/Backends/IDrawingBackend.cs | 20 +- .../Processing/Backends/NativeSurface.cs | 58 ++++ .../Processing/DrawingCanvas{TPixel}.cs | 47 +-- .../Drawing/FillPathProcessor{TPixel}.cs | 3 +- .../Backends/SkiaCoverageDrawingBackend.cs | 31 +- .../RasterizerDefaultsExtensionsTests.cs | 10 +- 12 files changed, 593 insertions(+), 100 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 583eb4615..7e9b2963d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// This backend intentionally preserves the contract used by -/// processors and DrawingCanvas<TPixel>. The public flow is identical to the default +/// processors and . The public flow is identical to the default /// backend: /// /// @@ -143,6 +143,8 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private int compositeSessionResourceWidth; private int compositeSessionResourceHeight; private TextureFormat compositeSessionResourceTextureFormat; + private bool compositeSessionRequiresReadback; + private bool compositeSessionOwnsTargetView; private Texture* coverageScratchMultisampleTexture; private TextureView* coverageScratchMultisampleView; private Texture* coverageScratchStencilTexture; @@ -226,16 +228,15 @@ private static void Trace(string message) /// Begins a composite session for a target region. /// /// - /// Nested calls are reference-counted. The first successful call uploads the target - /// pixels into a GPU texture. The final matching - /// flushes GPU results back to the target. + /// Nested calls are reference-counted. CPU targets are uploaded to a GPU session texture. + /// Native-surface targets bind directly to the surface view. /// - public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); if (this.compositeSessionDepth > 0) { @@ -256,7 +257,16 @@ public void BeginCompositeSession(Configuration configuration, Buffer2DR lock (this.gpuSync) { - bool started = this.TryBeginCompositeSessionCoreLocked(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); + bool started = false; + if (target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) + { + started = this.TryBeginCompositeSessionCoreLocked(cpuTarget, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); + } + else if (TryGetNativeSurfaceCapability(target, pixelHandler.TextureFormat, out WebGpuSurfaceCapability nativeSurfaceCapability) && + this.TryBeginCompositeSurfaceSessionCoreLocked(target, nativeSurfaceCapability)) + { + started = true; + } if (!started) { @@ -271,16 +281,16 @@ public void BeginCompositeSession(Configuration configuration, Buffer2DR /// Ends a previously started composite session. /// /// - /// When this is the outermost session and GPU work has modified the session texture, - /// the method performs one readback into the destination region, then clears active - /// session state. Session textures/buffers can be retained and reused by later sessions. + /// When this is the outermost session and GPU work has modified the active target, the + /// method either reads back into the CPU region (CPU session) or submits recorded commands + /// directly to the native surface (native session), then clears active session state. /// - public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + public void EndCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); if (this.compositeSessionDepth <= 0) { @@ -296,9 +306,22 @@ public void EndCompositeSession(Configuration configuration, Buffer2DReg lock (this.gpuSync) { Trace($"EndCompositeSession: gpuActive={this.compositeSessionGpuActive} dirty={this.compositeSessionDirty}"); - if (this.compositeSessionGpuActive && this.compositeSessionDirty) + if (this.compositeSessionGpuActive && + this.compositeSessionDirty) { - this.TryFlushCompositeSessionLocked(target); + if (this.compositeSessionRequiresReadback && + target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) + { + this.TryFlushCompositeSessionLocked(cpuTarget); + } + else if (!this.compositeSessionRequiresReadback) + { + this.TrySubmitCompositeSessionLocked(); + } + else + { + Trace("EndCompositeSession: skipped flush because CPU target was unavailable."); + } } this.ResetCompositeSessionStateLocked(); @@ -317,7 +340,7 @@ public void EndCompositeSession(Configuration configuration, Buffer2DReg /// public void FillPath( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, @@ -326,7 +349,7 @@ public void FillPath( { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); Guard.NotNull(path, nameof(path)); Guard.NotNull(brush, nameof(brush)); @@ -336,7 +359,7 @@ public void FillPath( return; } - Rectangle localTargetBounds = new(0, 0, target.Width, target.Height); + Rectangle localTargetBounds = new(0, 0, target.Bounds.Width, target.Bounds.Height); Rectangle clippedInterest = Rectangle.Intersect(localTargetBounds, rasterizerOptions.Interest); if (clippedInterest.Equals(Rectangle.Empty)) { @@ -381,11 +404,11 @@ public void FillPath( try { - Buffer2DRegion compositeTarget = target.GetSubRegion(clippedInterest); + ICanvasFrame compositeFrame = new CanvasRegionFrame(target, clippedInterest); bool openedCompositeSession = false; if (preparationMode == CoveragePreparationMode.Default && this.compositeSessionDepth == 0) { - this.BeginCompositeSession(configuration, compositeTarget); + this.BeginCompositeSession(configuration, compositeFrame); openedCompositeSession = true; } @@ -401,7 +424,7 @@ public void FillPath( this.CompositeCoverage( configuration, - compositeTarget, + compositeFrame, coverageHandle, Point.Empty, brush, @@ -418,7 +441,7 @@ public void FillPath( { if (openedCompositeSession) { - this.EndCompositeSession(configuration, compositeTarget); + this.EndCompositeSession(configuration, compositeFrame); } } } @@ -432,12 +455,12 @@ public void FillPath( /// Fills a rectangular region on the specified target region. /// /// - /// Rect fills are normalized through + /// Rect fills are normalized through /// so both APIs share the same coverage and composition paths. /// public void FillRegion( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, Brush brush, GraphicsOptions graphicsOptions, Rectangle region) @@ -445,7 +468,7 @@ public void FillRegion( { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); Guard.NotNull(brush, nameof(brush)); if (!CanUseGpuSession()) @@ -454,7 +477,7 @@ public void FillRegion( return; } - Rectangle localTargetBounds = new(0, 0, target.Width, target.Height); + Rectangle localTargetBounds = new(0, 0, target.Bounds.Width, target.Bounds.Height); Rectangle clippedRegion = Rectangle.Intersect(localTargetBounds, region); if (clippedRegion.Equals(Rectangle.Empty)) { @@ -635,7 +658,7 @@ private DrawingCoverageHandle PrepareCoverageFallback( /// public void CompositeCoverage( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, Brush brush, @@ -645,7 +668,7 @@ public void CompositeCoverage( { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); Guard.NotNull(brush, nameof(brush)); this.CompositeCoverageCallCount++; @@ -791,6 +814,32 @@ private static bool TryGetCompositePixelHandler(out CompositePixelRegist where TPixel : unmanaged, IPixel => CompositePixelHandlers.TryGetValue(typeof(TPixel), out pixelHandler); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetNativeSurfaceCapability( + ICanvasFrame target, + TextureFormat expectedTargetFormat, + out WebGpuSurfaceCapability capability) + where TPixel : unmanaged, IPixel + { + if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || nativeSurface is null) + { + capability = null!; + return false; + } + + if (!nativeSurface.TryGetCapability(out WebGpuSurfaceCapability? surfaceCapability) || + surfaceCapability is null || + surfaceCapability.TargetTextureView == 0 || + surfaceCapability.TargetFormat != expectedTargetFormat) + { + capability = null!; + return false; + } + + capability = surfaceCapability; + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasCompositePipelineForTextureFormat(TextureFormat textureFormat) { @@ -1801,7 +1850,7 @@ this.coverageCoverPipeline is null || return false; } - ulong vertexByteCount = checked((ulong)coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); + ulong vertexByteCount = checked(coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); if (!this.TryEnsureCoverageScratchVertexBufferLocked(vertexByteCount) || this.coverageScratchVertexBuffer is null) { return false; @@ -2277,11 +2326,76 @@ this.queue is null || this.compositeSessionTargetRectangle = target.Rectangle; this.compositeSessionTargetWidth = target.Width; this.compositeSessionTargetHeight = target.Height; + this.compositeSessionRequiresReadback = true; + this.compositeSessionOwnsTargetView = true; this.compositeSessionUniformWriteOffset = 0; this.compositeSessionDirty = false; return true; } + private bool TryBeginCompositeSurfaceSessionCoreLocked( + ICanvasFrame target, + WebGpuSurfaceCapability nativeSurfaceCapability) + where TPixel : unmanaged, IPixel + { + if (!this.IsGpuReady || + this.webGpu is null || + this.device is null || + this.queue is null || + nativeSurfaceCapability.TargetTextureView == 0 || + nativeSurfaceCapability.Device == 0 || + nativeSurfaceCapability.Queue == 0 || + target.Bounds.Width <= 0 || + target.Bounds.Height <= 0 || + target.Bounds.X < 0 || + target.Bounds.Y < 0) + { + return false; + } + + if (nativeSurfaceCapability.Device != (nint)this.device || + nativeSurfaceCapability.Queue != (nint)this.queue) + { + return false; + } + + if (target.Bounds.Right > nativeSurfaceCapability.Width || + target.Bounds.Bottom > nativeSurfaceCapability.Height) + { + return false; + } + + if (!this.TryGetOrCreateCompositePipelineLocked(nativeSurfaceCapability.TargetFormat, out _)) + { + return false; + } + + this.ResetCompositeSessionStateLocked(); + if (this.compositeSessionOwnsTargetView) + { + this.ReleaseTextureViewLocked(this.compositeSessionTargetView); + } + + this.ReleaseTextureLocked(this.compositeSessionTargetTexture); + this.compositeSessionTargetTexture = null; + this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); + this.compositeSessionReadbackBuffer = null; + this.compositeSessionReadbackBytesPerRow = 0; + this.compositeSessionReadbackByteCount = 0; + this.compositeSessionResourceWidth = 0; + this.compositeSessionResourceHeight = 0; + this.compositeSessionResourceTextureFormat = nativeSurfaceCapability.TargetFormat; + this.compositeSessionTargetView = (TextureView*)nativeSurfaceCapability.TargetTextureView; + this.compositeSessionOwnsTargetView = false; + this.compositeSessionRequiresReadback = false; + this.compositeSessionTargetRectangle = target.Bounds; + this.compositeSessionTargetWidth = target.Bounds.Width; + this.compositeSessionTargetHeight = target.Bounds.Height; + this.compositeSessionUniformWriteOffset = 0; + this.compositeSessionDirty = false; + return this.TryEnsureCompositeSessionUniformBufferLocked(); + } + private bool TryEnsureCompositeSessionResourcesLocked( int width, int height, @@ -2388,9 +2502,33 @@ this.compositeSessionUniformBuffer is not null && this.compositeSessionResourceWidth = width; this.compositeSessionResourceHeight = height; this.compositeSessionResourceTextureFormat = textureFormat; + this.compositeSessionRequiresReadback = true; + this.compositeSessionOwnsTargetView = true; return true; } + private bool TryEnsureCompositeSessionUniformBufferLocked() + { + if (this.compositeSessionUniformBuffer is not null) + { + return true; + } + + if (this.webGpu is null || this.device is null) + { + return false; + } + + BufferDescriptor uniformBufferDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = CompositeUniformBufferSize + }; + + this.compositeSessionUniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); + return this.compositeSessionUniformBuffer is not null; + } + /// /// Reads the session target texture back into the canvas region. /// @@ -2494,6 +2632,56 @@ this.compositeSessionReadbackBuffer is null || this.webGpu.CommandBufferRelease(commandBuffer); } + if (commandEncoder is not null) + { + if (this.compositeSessionCommandEncoder == commandEncoder) + { + this.compositeSessionCommandEncoder = null; + } + + this.webGpu.CommandEncoderRelease(commandEncoder); + } + } + } + + private bool TrySubmitCompositeSessionLocked() + { + if (this.webGpu is null || this.device is null || this.queue is null) + { + return false; + } + + CommandEncoder* commandEncoder = this.compositeSessionCommandEncoder; + CommandBuffer* commandBuffer = null; + try + { + this.TryCloseCompositeSessionPassLocked(); + + if (commandEncoder is null) + { + return true; + } + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + this.compositeSessionCommandEncoder = null; + this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); + this.webGpu.CommandBufferRelease(commandBuffer); + commandBuffer = null; + return true; + } + finally + { + if (commandBuffer is not null) + { + this.webGpu.CommandBufferRelease(commandBuffer); + } + if (commandEncoder is not null) { this.webGpu.CommandEncoderRelease(commandEncoder); @@ -2514,6 +2702,7 @@ private void ResetCompositeSessionStateLocked() this.compositeSessionTargetRectangle = default; this.compositeSessionTargetWidth = 0; this.compositeSessionTargetHeight = 0; + this.compositeSessionRequiresReadback = false; this.compositeSessionDirty = false; } @@ -2534,13 +2723,19 @@ private void ReleaseCompositeSessionResourcesLocked() this.ReleaseAllCoverageCompositeBindGroupsLocked(); this.ReleaseBufferLocked(this.compositeSessionUniformBuffer); this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); - this.ReleaseTextureViewLocked(this.compositeSessionTargetView); + if (this.compositeSessionOwnsTargetView) + { + this.ReleaseTextureViewLocked(this.compositeSessionTargetView); + } + this.ReleaseTextureLocked(this.compositeSessionTargetTexture); this.compositeSessionUniformBuffer = null; this.compositeSessionUniformWriteOffset = 0; this.compositeSessionReadbackBuffer = null; this.compositeSessionTargetTexture = null; this.compositeSessionTargetView = null; + this.compositeSessionRequiresReadback = false; + this.compositeSessionOwnsTargetView = false; this.compositeSessionReadbackBytesPerRow = 0; this.compositeSessionReadbackByteCount = 0; this.compositeSessionResourceWidth = 0; @@ -2549,7 +2744,7 @@ private void ReleaseCompositeSessionResourcesLocked() } private bool TryCompositeCoverageGpu( - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, WebGpuBrushData brushData, @@ -2571,7 +2766,7 @@ private bool TryCompositeCoverageGpu( return false; } - if (target.Width <= 0 || target.Height <= 0) + if (target.Bounds.Width <= 0 || target.Bounds.Height <= 0) { return true; } @@ -2581,15 +2776,13 @@ private bool TryCompositeCoverageGpu( return true; } - int compositeWidth = Math.Min(target.Width, entry.Width - sourceOffset.X); - int compositeHeight = Math.Min(target.Height, entry.Height - sourceOffset.Y); + int compositeWidth = Math.Min(target.Bounds.Width, entry.Width - sourceOffset.X); + int compositeHeight = Math.Min(target.Bounds.Height, entry.Height - sourceOffset.Y); if (compositeWidth <= 0 || compositeHeight <= 0) { return true; } - Buffer2DRegion destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); - lock (this.gpuSync) { if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || @@ -2604,7 +2797,6 @@ private bool TryCompositeCoverageGpu( } if (this.compositeSessionGpuActive && - this.compositeSessionTargetTexture is not null && this.compositeSessionTargetView is not null) { RenderPipeline* compositePipeline = this.GetCompositeSessionPipelineLocked(); @@ -2613,8 +2805,8 @@ this.compositeSessionTargetTexture is not null && return false; } - int destinationX = destinationRegion.Rectangle.X - this.compositeSessionTargetRectangle.X; - int destinationY = destinationRegion.Rectangle.Y - this.compositeSessionTargetRectangle.Y; + int destinationX = target.Bounds.X - this.compositeSessionTargetRectangle.X; + int destinationY = target.Bounds.Y - this.compositeSessionTargetRectangle.Y; if ((uint)destinationX >= (uint)this.compositeSessionTargetWidth || (uint)destinationY >= (uint)this.compositeSessionTargetHeight) { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs new file mode 100644 index 000000000..788ea67ca --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Native WebGPU surface capability attached to . +/// +public sealed class WebGpuSurfaceCapability +{ + /// + /// Initializes a new instance of the class. + /// + /// Opaque WGPUDevice* handle. + /// Opaque WGPUQueue* handle. + /// Opaque WGPUTextureView* handle for the current frame. + /// Native render target texture format. + /// Surface width in pixels. + /// Surface height in pixels. + /// Whether the target format is sRGB encoded. + /// Whether alpha is premultiplied in the target surface. + public WebGpuSurfaceCapability( + nint device, + nint queue, + nint targetTextureView, + TextureFormat targetFormat, + int width, + int height, + bool isSrgb, + bool isPremultipliedAlpha) + { + this.Device = device; + this.Queue = queue; + this.TargetTextureView = targetTextureView; + this.TargetFormat = targetFormat; + this.Width = width; + this.Height = height; + this.IsSrgb = isSrgb; + this.IsPremultipliedAlpha = isPremultipliedAlpha; + } + + /// + /// Gets the opaque WGPUDevice* handle. + /// + public nint Device { get; } + + /// + /// Gets the opaque WGPUQueue* handle. + /// + public nint Queue { get; } + + /// + /// Gets the opaque WGPUTextureView* handle for the current frame. + /// + public nint TargetTextureView { get; } + + /// + /// Gets the native render target texture format. + /// + public TextureFormat TargetFormat { get; } + + /// + /// Gets the surface width in pixels. + /// + public int Width { get; } + + /// + /// Gets the surface height in pixels. + /// + public int Height { get; } + + /// + /// Gets a value indicating whether the target format is sRGB encoded. + /// + public bool IsSrgb { get; } + + /// + /// Gets a value indicating whether the target uses premultiplied alpha. + /// + public bool IsPremultipliedAlpha { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs new file mode 100644 index 000000000..2714017f2 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Frame adapter that exposes a clipped subregion of another frame. +/// +/// The pixel format. +internal sealed class CanvasRegionFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly ICanvasFrame parent; + private readonly Rectangle region; + + public CanvasRegionFrame(ICanvasFrame parent, Rectangle region) + { + Guard.NotNull(parent, nameof(parent)); + Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region)); + Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region)); + this.parent = parent; + this.region = region; + } + + public Rectangle Bounds => new( + this.parent.Bounds.X + this.region.X, + this.parent.Bounds.Y + this.region.Y, + this.region.Width, + this.region.Height); + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + if (!this.parent.TryGetCpuRegion(out Buffer2DRegion parentRegion)) + { + region = default; + return false; + } + + region = parentRegion.GetSubRegion(this.region); + return true; + } + + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + => this.parent.TryGetNativeSurface(out surface); +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs new file mode 100644 index 000000000..29eefb190 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Canvas frame adapter over a CPU . +/// +/// The pixel format. +internal sealed class CpuCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly Buffer2DRegion region; + private readonly NativeSurface? nativeSurface; + + public CpuCanvasFrame(Buffer2DRegion region, NativeSurface? nativeSurface = null) + { + Guard.NotNull(region.Buffer, nameof(region)); + this.region = region; + this.nativeSurface = nativeSurface; + } + + public Rectangle Bounds => this.region.Rectangle; + + public bool TryGetCpuRegion(out Buffer2DRegion cpuRegion) + { + cpuRegion = this.region; + return true; + } + + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = this.nativeSurface; + return surface is not null; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index c3c58e229..3456b9562 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -52,49 +52,71 @@ public static DefaultDrawingBackend Create(IRasterizer rasterizer) } /// - public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); } /// - public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + public void EndCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); } /// public void FillPath( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, in RasterizerOptions rasterizerOptions) where TPixel : unmanaged, IPixel - => FillPath( + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target, nameof(target)); + + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException( + $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.FillPath)}."); + } + + FillPath( configuration, - target, + destinationRegion, path, brush, graphicsOptions, rasterizerOptions, configuration.MemoryAllocator, this.PrimaryRasterizer); + } /// public void FillRegion( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, Brush brush, GraphicsOptions graphicsOptions, Rectangle region) where TPixel : unmanaged, IPixel - => FillRegionCore(configuration, target, brush, graphicsOptions, region); + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target, nameof(target)); + + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException( + $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.FillRegion)}."); + } + + FillRegionCore(configuration, destinationRegion, brush, graphicsOptions, region); + } /// public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) @@ -140,7 +162,7 @@ public DrawingCoverageHandle PrepareCoverage( /// public void CompositeCoverage( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, Brush brush, @@ -149,7 +171,7 @@ public void CompositeCoverage( where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target.Buffer, nameof(target)); + Guard.NotNull(target, nameof(target)); Guard.NotNull(brush, nameof(brush)); if (!coverageHandle.IsValid) @@ -157,13 +179,19 @@ public void CompositeCoverage( return; } + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) + { + throw new NotSupportedException( + $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.CompositeCoverage)}."); + } + if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out Buffer2D? coverageMap)) { throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); } if (!CoverageCompositor.TryGetCompositeRegions( - target, + destinationFrame, coverageMap, sourceOffset, out Buffer2DRegion destinationRegion, diff --git a/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs new file mode 100644 index 000000000..f3cb2e795 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Per-frame destination for . +/// +/// The pixel format. +public interface ICanvasFrame + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the frame bounds in root target coordinates. + /// + public Rectangle Bounds { get; } + + /// + /// Attempts to get a CPU-accessible destination region. + /// + /// The CPU region when available. + /// when a CPU region is available. + public bool TryGetCpuRegion(out Buffer2DRegion region); + + /// + /// Attempts to get an opaque native destination surface. + /// + /// The native surface when available. + /// when a native surface is available. + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface); +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 5e9b5aaed..53f3b34e8 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -24,8 +24,8 @@ internal interface IDrawingBackend /// /// The pixel format. /// Active processing configuration. - /// Destination target region. - public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + /// Destination frame. + public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel; /// @@ -33,8 +33,8 @@ public void BeginCompositeSession(Configuration configuration, Buffer2DR /// /// The pixel format. /// Active processing configuration. - /// Destination target region. - public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + /// Destination frame. + public void EndCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel; /// @@ -42,14 +42,14 @@ public void EndCompositeSession(Configuration configuration, Buffer2DReg /// /// The pixel format. /// Active processing configuration. - /// Destination target region. + /// Destination frame. /// Path in target-local coordinates. /// Brush used to shade covered pixels. /// Graphics blending/composition options. /// Rasterizer options in target-local coordinates. public void FillPath( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, @@ -61,13 +61,13 @@ public void FillPath( /// /// The pixel format. /// Active processing configuration. - /// Destination target region. + /// Destination frame. /// Brush used to shade destination pixels. /// Graphics blending/composition options. /// Region in target-local coordinates. public void FillRegion( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, Brush brush, GraphicsOptions graphicsOptions, Rectangle region) @@ -103,7 +103,7 @@ public DrawingCoverageHandle PrepareCoverage( /// /// The pixel format. /// Active processing configuration. - /// Destination target region. + /// Destination frame. /// Handle to prepared coverage data. /// Source offset inside the prepared coverage. /// Brush used to shade destination pixels. @@ -111,7 +111,7 @@ public DrawingCoverageHandle PrepareCoverage( /// Brush bounds used when creating the applicator. public void CompositeCoverage( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, Brush brush, diff --git a/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs new file mode 100644 index 000000000..5791ae03a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Opaque native destination with backend capability attachments. +/// +public sealed class NativeSurface +{ + private readonly ConcurrentDictionary capabilities = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Pixel format information for the destination surface. + public NativeSurface(PixelTypeInfo pixelType) + => this.PixelType = pixelType; + + /// + /// Gets pixel format information for this destination surface. + /// + public PixelTypeInfo PixelType { get; } + + /// + /// Sets or replaces a capability object. + /// + /// Capability type. + /// Capability instance. + public void SetCapability(TCapability capability) + where TCapability : class + { + Guard.NotNull(capability, nameof(capability)); + this.capabilities[typeof(TCapability)] = capability; + } + + /// + /// Attempts to get a capability object by type. + /// + /// Capability type. + /// Capability instance when available. + /// when found. + public bool TryGetCapability([NotNullWhen(true)] out TCapability? capability) + where TCapability : class + { + if (this.capabilities.TryGetValue(typeof(TCapability), out object? value) && value is TCapability typed) + { + capability = typed; + return true; + } + + capability = null; + return false; + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index ad464d601..19f9c51e7 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// A drawing canvas over a pixel buffer region. +/// A drawing canvas over a frame target. /// /// The pixel format. public sealed class DrawingCanvas : IDisposable @@ -22,7 +22,7 @@ public sealed class DrawingCanvas : IDisposable { private readonly Configuration configuration; private readonly IDrawingBackend backend; - private readonly Buffer2DRegion targetRegion; + private readonly ICanvasFrame targetFrame; private bool isDisposed; /// @@ -31,23 +31,33 @@ public sealed class DrawingCanvas : IDisposable /// The active processing configuration. /// The destination target region. public DrawingCanvas(Configuration configuration, Buffer2DRegion targetRegion) - : this(configuration, configuration.GetDrawingBackend(), targetRegion) + : this(configuration, new CpuCanvasFrame(targetRegion)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// The destination frame. + public DrawingCanvas(Configuration configuration, ICanvasFrame targetFrame) + : this(configuration, configuration.GetDrawingBackend(), targetFrame) { } internal DrawingCanvas( Configuration configuration, IDrawingBackend backend, - Buffer2DRegion targetRegion) + ICanvasFrame targetFrame) { Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(targetRegion.Buffer, nameof(targetRegion)); Guard.NotNull(backend, nameof(backend)); + Guard.NotNull(targetFrame, nameof(targetFrame)); this.configuration = configuration; this.backend = backend; - this.targetRegion = targetRegion; - this.Bounds = new Rectangle(0, 0, targetRegion.Width, targetRegion.Height); + this.targetFrame = targetFrame; + this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); } /// @@ -65,8 +75,8 @@ public DrawingCanvas CreateRegion(Rectangle region) this.EnsureNotDisposed(); Rectangle clipped = Rectangle.Intersect(this.Bounds, region); - Buffer2DRegion childRegion = this.targetRegion.GetSubRegion(clipped); - return new DrawingCanvas(this.configuration, this.backend, childRegion); + ICanvasFrame childFrame = new CanvasRegionFrame(this.targetFrame, clipped); + return new DrawingCanvas(this.configuration, this.backend, childFrame); } /// @@ -88,8 +98,7 @@ public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOp this.EnsureNotDisposed(); Guard.NotNull(brush, nameof(brush)); Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); - - this.backend.FillRegion(this.configuration, this.targetRegion, brush, graphicsOptions, region); + this.backend.FillRegion(this.configuration, this.targetFrame, brush, graphicsOptions, region); } /// @@ -135,7 +144,7 @@ internal void FillPath( rasterizationMode, samplingOrigin); - this.backend.FillPath(this.configuration, this.targetRegion, path, brush, graphicsOptions, rasterizerOptions); + this.backend.FillPath(this.configuration, this.targetFrame, path, brush, graphicsOptions, rasterizerOptions); } /// @@ -207,7 +216,7 @@ private void DrawTextOperations(IEnumerable operations, Drawin Guard.NotNull(drawingOptions, nameof(drawingOptions)); Dictionary coverageCache = []; - this.backend.BeginCompositeSession(this.configuration, this.targetRegion); + this.backend.BeginCompositeSession(this.configuration, this.targetFrame); try { // Operations are layered by render pass (fill, outline, decorations). @@ -239,7 +248,7 @@ private void DrawTextOperations(IEnumerable operations, Drawin if (!this.TryGetCompositeRegion( coverageLocation, coverageEntry.RasterizedSize, - out Buffer2DRegion compositeRegion, + out Rectangle compositeRegion, out Point sourceOffset)) { continue; @@ -247,17 +256,17 @@ private void DrawTextOperations(IEnumerable operations, Drawin this.backend.CompositeCoverage( this.configuration, - compositeRegion, + new CanvasRegionFrame(this.targetFrame, compositeRegion), coverageEntry.CoverageHandle, sourceOffset, compositeBrush, graphicsOptions, - this.targetRegion.Rectangle); + this.Bounds); } } finally { - this.backend.EndCompositeSession(this.configuration, this.targetRegion); + this.backend.EndCompositeSession(this.configuration, this.targetFrame); foreach ((_, CoverageCacheEntry coverageEntry) in coverageCache) { @@ -400,7 +409,7 @@ private static bool TryCreateCoveragePath( private bool TryGetCompositeRegion( Point coverageLocation, Size coverageSize, - out Buffer2DRegion compositeRegion, + out Rectangle compositeRegion, out Point sourceOffset) { Rectangle destination = new(coverageLocation, coverageSize); @@ -413,7 +422,7 @@ private bool TryGetCompositeRegion( } sourceOffset = new Point(clipped.X - destination.X, clipped.Y - destination.Y); - compositeRegion = this.targetRegion.GetSubRegion(clipped); + compositeRegion = clipped; return true; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index a1ea7aaad..62541f743 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; @@ -57,7 +58,7 @@ protected override void OnFrameApply(ImageFrame source) using DrawingCanvas canvas = new( configuration, - new(source.PixelBuffer, source.Bounds)); + new Buffer2DRegion(source.PixelBuffer, source.Bounds)); canvas.FillPath(this.path, brush, this.definition.Options, this.definition.SamplingOrigin); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 442658d17..d6f93d0d4 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -26,19 +26,19 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; - public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { } - public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + public void EndCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { } public void FillPath( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, @@ -54,7 +54,7 @@ public void FillPath( public void FillRegion( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, Brush brush, GraphicsOptions graphicsOptions, Rectangle region) @@ -125,7 +125,7 @@ public DrawingCoverageHandle PrepareCoverage( public void CompositeCoverage( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, Brush brush, @@ -135,11 +135,6 @@ public void CompositeCoverage( { ArgumentNullException.ThrowIfNull(configuration); - if (target.Buffer is null) - { - throw new ArgumentNullException(nameof(target)); - } - ArgumentNullException.ThrowIfNull(brush); this.CompositeCoverageCallCount++; @@ -154,6 +149,12 @@ public void CompositeCoverage( throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); } + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException( + $"{nameof(SkiaCoverageDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.CompositeCoverage)}."); + } + if (bitmap.ColorType != SKColorType.Alpha8) { throw new InvalidOperationException($"Prepared coverage '{coverageHandle.Value}' is not Alpha8."); @@ -164,8 +165,8 @@ public void CompositeCoverage( return; } - int compositeWidth = Math.Min(target.Width, bitmap.Width - sourceOffset.X); - int compositeHeight = Math.Min(target.Height, bitmap.Height - sourceOffset.Y); + int compositeWidth = Math.Min(destinationRegion.Width, bitmap.Width - sourceOffset.X); + int compositeHeight = Math.Min(destinationRegion.Height, bitmap.Height - sourceOffset.Y); if (compositeWidth <= 0 || compositeHeight <= 0) { return; @@ -174,13 +175,13 @@ public void CompositeCoverage( using BrushApplicator applicator = brush.CreateApplicator( configuration, graphicsOptions, - target, + destinationRegion, brushBounds); ReadOnlySpan source = bitmap.GetPixelSpan(); int rowBytes = bitmap.RowBytes; - int absoluteX = target.Rectangle.X; - int absoluteY = target.Rectangle.Y; + int absoluteX = destinationRegion.Rectangle.X; + int absoluteY = destinationRegion.Rectangle.Y; float[] rented = ArrayPool.Shared.Rent(compositeWidth); try diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 3effa1a4a..d206e6905 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -109,19 +109,19 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { - public void BeginCompositeSession(Configuration configuration, Buffer2DRegion target) + public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { } - public void EndCompositeSession(Configuration configuration, Buffer2DRegion target) + public void EndCompositeSession(Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { } public void FillPath( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, @@ -132,7 +132,7 @@ public void FillPath( public void FillRegion( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, Brush brush, GraphicsOptions graphicsOptions, Rectangle region) @@ -160,7 +160,7 @@ public DrawingCoverageHandle PrepareCoverage( public void CompositeCoverage( Configuration configuration, - Buffer2DRegion target, + ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, Brush brush, From 4414c880d8d5a7c6c14015c73c1523ab48c91443 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 22 Feb 2026 02:16:03 +1000 Subject: [PATCH 007/136] Refactor and simplify --- ...{WebGpuBrushData.cs => WebGPUBrushData.cs} | 13 +- .../WebGPUDrawingBackend.cs | 2389 +++++------------ .../WebGPURasterizer.cs | 1059 ++++++++ .../{WebGpuRuntime.cs => WebGPURuntime.cs} | 4 +- ...pability.cs => WebGPUSurfaceCapability.cs} | 14 +- .../Processing/Backends/CompositionCommand.cs | 134 + .../Backends/DefaultDrawingBackend.cs | 589 +--- .../Processing/Backends/IDrawingBackend.cs | 91 +- .../DrawingCanvasBatcher{TPixel}.cs | 31 + .../Processing/DrawingCanvas{TPixel}.cs | 316 +-- .../Drawing/DrawPolygon.cs | 4 +- .../Backends/SkiaCoverageDrawingBackend.cs | 59 +- .../Backends/WebGPUDrawingBackendTests.cs | 22 +- .../RasterizerDefaultsExtensionsTests.cs | 53 +- 14 files changed, 2067 insertions(+), 2711 deletions(-) rename src/ImageSharp.Drawing.WebGPU/Brushes/{WebGpuBrushData.cs => WebGPUBrushData.cs} (60%) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs rename src/ImageSharp.Drawing.WebGPU/{WebGpuRuntime.cs => WebGPURuntime.cs} (98%) rename src/ImageSharp.Drawing.WebGPU/{WebGpuSurfaceCapability.cs => WebGPUSurfaceCapability.cs} (83%) create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs create mode 100644 src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs similarity index 60% rename from src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs rename to src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs index 520dd93bb..d1af7ad20 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGpuBrushData.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs @@ -5,30 +5,31 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -internal enum WebGpuBrushKind : uint +internal enum WebGPUBrushKind : uint { SolidColor = 0 } -internal readonly struct WebGpuBrushData +internal readonly struct WebGPUBrushData { - public WebGpuBrushData(WebGpuBrushKind kind, Vector4 solidColor) + public WebGPUBrushData(WebGPUBrushKind kind, Vector4 solidColor) { this.Kind = kind; this.SolidColor = solidColor; } - public WebGpuBrushKind Kind { get; } + public WebGPUBrushKind Kind { get; } public Vector4 SolidColor { get; } - public static bool TryCreate(Brush brush, out WebGpuBrushData brushData) + public static bool TryCreate(Brush brush, Rectangle brushBounds, out WebGPUBrushData brushData) { Guard.NotNull(brush, nameof(brush)); + _ = brushBounds; if (brush is SolidBrush solidBrush) { - brushData = new WebGpuBrushData(WebGpuBrushKind.SolidColor, solidBrush.Color.ToScaledVector4()); + brushData = new WebGPUBrushData(WebGPUBrushKind.SolidColor, solidBrush.Color.ToScaledVector4()); return true; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 7e9b2963d..d495ff051 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -21,96 +21,35 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// WebGPU-backed implementation of . /// /// -/// -/// This backend intentionally preserves the contract used by -/// processors and . The public flow is identical to the default -/// backend: -/// +/// The public flow mirrors : /// -/// Prepare path coverage into a reusable handle. -/// Composite prepared coverage into a target region using brush + graphics options. -/// Release coverage handle resources deterministically. +/// FillPath enqueues normalized composition commands. +/// FlushCompositions executes the queued commands in order. /// -/// -/// The implementation detail differs: coverage preparation is accelerated through WebGPU render -/// passes while composition uses a dedicated blend shader targeting Rgba8Unorm. -/// -/// -/// Internally, the backend is split into two independent phases: -/// -/// -/// -/// Coverage preparation: -/// path geometry is flattened in local-interest coordinates, converted to edge triangles, -/// then rasterized by a stencil-and-cover render pass into an R8Unorm coverage mask. -/// This avoids per-pixel edge scans in shader code. -/// -/// -/// Coverage composition: -/// a composition shader samples the prepared coverage mask and applies brush/blend rules into -/// an Rgba8Unorm target texture using source-over semantics. -/// -/// -/// -/// Coverage rasterization supports both fill rules: -/// and . -/// The active rule selects the appropriate stencil pipeline at draw time. -/// -/// -/// Composition runs in session mode: -/// the target region is uploaded once, multiple composite operations execute on the same GPU -/// texture, then one readback copies results to the destination buffer. -/// -/// -/// Threading model: all GPU object creation, command encoding, submission, and map/readback are -/// synchronized by . This keeps native resource lifetime deterministic and -/// prevents command submission races while still allowing concurrent high-level calls. -/// -/// -/// Handle ownership model: prepared coverage is stored in and owned -/// by this backend instance. The caller receives only an opaque . -/// Releasing the handle always releases the corresponding GPU texture/view (or fallback handle). -/// -/// -/// Sampling model: path geometry is translated to local interest space and adjusted for -/// before rasterization so coverage generation remains -/// consistent with canvas-local coordinate semantics. -/// -/// -/// If a GPU path is unavailable for the current operation (unsupported pixel/brush/blend mode -/// or initialization failure), behavior falls back to so -/// output remains deterministic and API semantics stay consistent. -/// +/// GPU execution prepares coverage once (stencil-and-cover into R8 coverage), then composites all +/// queued commands against the active target session. If the pixel type is unsupported for GPU, +/// the whole flush delegates to . /// internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; private const uint CompositeUniformAlignment = 256; private const uint CompositeUniformBufferSize = 256 * 1024; - private const uint CoverageCoverVertexCount = 3; - private const uint CoverageSampleCount = 4; private const int CallbackTimeoutMilliseconds = 10_000; private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; - private static ReadOnlySpan CoverageStencilVertexEntryPoint => "vs_edge\0"u8; - - private static ReadOnlySpan CoverageStencilFragmentEntryPoint => "fs_stencil\0"u8; - - private static ReadOnlySpan CoverageCoverVertexEntryPoint => "vs_cover\0"u8; - - private static ReadOnlySpan CoverageCoverFragmentEntryPoint => "fs_cover\0"u8; - private readonly object gpuSync = new(); private readonly ConcurrentDictionary preparedCoverage = new(); private readonly DefaultDrawingBackend fallbackBackend; + private WebGPURasterizer? coverageRasterizer; private int nextCoverageHandleId; private bool isDisposed; - private WebGpuRuntime.Lease? runtimeLease; - private WebGPU? webGpu; + private WebGPURuntime.Lease? runtimeLease; + private WebGPU? webGPU; private Wgpu? wgpuExtension; private Instance* instance; private Adapter* adapter; @@ -119,19 +58,13 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private BindGroupLayout* compositeBindGroupLayout; private PipelineLayout* compositePipelineLayout; private readonly ConcurrentDictionary compositePipelines = new(); - private PipelineLayout* coveragePipelineLayout; - private RenderPipeline* coverageStencilEvenOddPipeline; - private RenderPipeline* coverageStencilNonZeroIncrementPipeline; - private RenderPipeline* coverageStencilNonZeroDecrementPipeline; - private RenderPipeline* coverageCoverPipeline; private int compositeSessionDepth; - private bool compositeSessionGpuActive; + private bool compositeSessionGPUActive; private bool compositeSessionDirty; + private readonly List compositeSessionCommands = []; private RenderPassEncoder* compositeSessionPassEncoder; private Rectangle compositeSessionTargetRectangle; - private int compositeSessionTargetWidth; - private int compositeSessionTargetHeight; private Texture* compositeSessionTargetTexture; private TextureView* compositeSessionTargetView; private WgpuBuffer* compositeSessionReadbackBuffer; @@ -145,21 +78,22 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private TextureFormat compositeSessionResourceTextureFormat; private bool compositeSessionRequiresReadback; private bool compositeSessionOwnsTargetView; - private Texture* coverageScratchMultisampleTexture; - private TextureView* coverageScratchMultisampleView; - private Texture* coverageScratchStencilTexture; - private TextureView* coverageScratchStencilView; - private int coverageScratchWidth; - private int coverageScratchHeight; - private WgpuBuffer* coverageScratchVertexBuffer; - private ulong coverageScratchVertexCapacityBytes; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); private static readonly bool TraceEnabled = string.Equals( Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), "1", StringComparison.Ordinal); - public WebGPUDrawingBackend() => this.fallbackBackend = DefaultDrawingBackend.Instance; + public WebGPUDrawingBackend() + { + this.fallbackBackend = DefaultDrawingBackend.Instance; + lock (this.gpuSync) + { + this.GPUInitializationAttempted = true; + this.LastGPUInitializationFailure = null; + this.IsGPUReady = this.TryInitializeGPULocked(); + } + } private static void Trace(string message) { @@ -177,7 +111,7 @@ private static void Trace(string message) /// /// Gets the number of coverage preparations executed on the GPU. /// - public int GpuPrepareCoverageCallCount { get; private set; } + public int GPUPrepareCoverageCallCount { get; private set; } /// /// Gets the number of coverage preparations delegated to the fallback backend. @@ -192,7 +126,7 @@ private static void Trace(string message) /// /// Gets the number of compositions executed on the GPU. /// - public int GpuCompositeCoverageCallCount { get; private set; } + public int GPUCompositeCoverageCallCount { get; private set; } /// /// Gets the number of compositions delegated to the fallback backend. @@ -207,17 +141,17 @@ private static void Trace(string message) /// /// Gets a value indicating whether the backend completed GPU initialization. /// - public bool IsGpuReady { get; private set; } + public bool IsGPUReady { get; private set; } /// /// Gets a value indicating whether GPU initialization has been attempted. /// - public bool GpuInitializationAttempted { get; private set; } + public bool GPUInitializationAttempted { get; private set; } /// /// Gets the last GPU initialization failure reason, if any. /// - public string? LastGpuInitializationFailure { get; private set; } + public string? LastGPUInitializationFailure { get; private set; } /// /// Gets the number of prepared coverage entries currently cached by handle. @@ -245,36 +179,25 @@ public void BeginCompositeSession(Configuration configuration, ICanvasFr } this.compositeSessionDepth = 1; - this.compositeSessionGpuActive = false; + this.compositeSessionGPUActive = false; this.compositeSessionDirty = false; + this.compositeSessionCommands.Clear(); - if (!TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler) || - !this.TryEnsureGpuReady() || - !this.HasCompositePipelineForTextureFormat(pixelHandler.TextureFormat)) + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler) || + !this.IsGPUReady) { return; } lock (this.gpuSync) { - bool started = false; - if (target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) - { - started = this.TryBeginCompositeSessionCoreLocked(cpuTarget, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); - } - else if (TryGetNativeSurfaceCapability(target, pixelHandler.TextureFormat, out WebGpuSurfaceCapability nativeSurfaceCapability) && - this.TryBeginCompositeSurfaceSessionCoreLocked(target, nativeSurfaceCapability)) - { - started = true; - } - - if (!started) + if (!this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _)) { return; } - - this.compositeSessionGpuActive = true; } + + this.ActivateCompositeSession(target, pixelHandler); } /// @@ -305,11 +228,15 @@ public void EndCompositeSession(Configuration configuration, ICanvasFram lock (this.gpuSync) { - Trace($"EndCompositeSession: gpuActive={this.compositeSessionGpuActive} dirty={this.compositeSessionDirty}"); - if (this.compositeSessionGpuActive && + Trace($"EndCompositeSession: gpuActive={this.compositeSessionGPUActive} dirty={this.compositeSessionDirty}"); + if (this.compositeSessionGPUActive && this.compositeSessionDirty) { - if (this.compositeSessionRequiresReadback && + if (!this.TryDrainQueuedCompositeCommandsLocked()) + { + throw new InvalidOperationException("Failed to encode queued GPU composite commands."); + } + else if (this.compositeSessionRequiresReadback && target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) { this.TryFlushCompositeSessionLocked(cpuTarget); @@ -327,7 +254,7 @@ public void EndCompositeSession(Configuration configuration, ICanvasFram this.ResetCompositeSessionStateLocked(); } - this.compositeSessionGpuActive = false; + this.compositeSessionGPUActive = false; this.compositeSessionDirty = false; } @@ -344,188 +271,171 @@ public void FillPath( IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) + in RasterizerOptions rasterizerOptions, + DrawingCanvasBatcher batcher) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - Guard.NotNull(path, nameof(path)); - Guard.NotNull(brush, nameof(brush)); - - if (!CanUseGpuSession()) - { - this.fallbackBackend.FillPath(configuration, target, path, brush, graphicsOptions, rasterizerOptions); - return; - } + batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); + } - Rectangle localTargetBounds = new(0, 0, target.Bounds.Width, target.Bounds.Height); - Rectangle clippedInterest = Rectangle.Intersect(localTargetBounds, rasterizerOptions.Interest); - if (clippedInterest.Equals(Rectangle.Empty)) + /// + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + IReadOnlyList compositions) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + if (compositions.Count == 0) { return; } - RasterizerOptions clippedOptions = clippedInterest.Equals(rasterizerOptions.Interest) - ? rasterizerOptions - : new RasterizerOptions( - clippedInterest, - rasterizerOptions.IntersectionRule, - rasterizerOptions.RasterizationMode, - rasterizerOptions.SamplingOrigin); - - CoveragePreparationMode preparationMode = - this.SupportsCoverageComposition(brush, graphicsOptions) - ? CoveragePreparationMode.Default - : CoveragePreparationMode.Fallback; + CompositionCommand coverageDefinition = compositions[0]; + ICanvasFrame compositeFrame = new CanvasRegionFrame(target, coverageDefinition.RasterizerOptions.Interest); + bool useGPUPath = this.TryResolveGPUFlush(out CompositePixelRegistration pixelHandler); + bool openedCompositeSession = false; + DrawingCoverageHandle coverageHandle = default; - long prepareStart = 0; - if (TraceEnabled) + if (useGPUPath) { - prepareStart = Stopwatch.GetTimestamp(); - } - - DrawingCoverageHandle coverageHandle = this.PrepareCoverage( - path, - clippedOptions, - configuration.MemoryAllocator, - preparationMode); + if (this.compositeSessionDepth == 0) + { + this.compositeSessionDepth = 1; + this.compositeSessionGPUActive = false; + this.compositeSessionDirty = false; + this.compositeSessionCommands.Clear(); - if (TraceEnabled) - { - double prepareMs = Stopwatch.GetElapsedTime(prepareStart).TotalMilliseconds; - Trace($"FillPath: prepare={prepareMs:F3}ms mode={preparationMode}"); + useGPUPath = this.ActivateCompositeSession(compositeFrame, pixelHandler); + openedCompositeSession = true; + } + else + { + useGPUPath = this.compositeSessionGPUActive; + } } - if (!coverageHandle.IsValid) + if (useGPUPath) { - return; + coverageHandle = this.PrepareCoverage( + coverageDefinition.Path, + coverageDefinition.RasterizerOptions, + configuration.MemoryAllocator, + CoveragePreparationMode.Default); + useGPUPath = coverageHandle.IsValid; } - try + if (!useGPUPath) { - ICanvasFrame compositeFrame = new CanvasRegionFrame(target, clippedInterest); - bool openedCompositeSession = false; - if (preparationMode == CoveragePreparationMode.Default && this.compositeSessionDepth == 0) + if (openedCompositeSession) { - this.BeginCompositeSession(configuration, compositeFrame); - openedCompositeSession = true; + this.EndCompositeSession(configuration, compositeFrame); } - Rectangle brushBounds = Rectangle.Ceiling(path.Bounds); + this.FlushCompositionsFallback(configuration, target, compositions); + return; + } - try + try + { + for (int i = 0; i < compositions.Count; i++) { - long compositeStart = 0; - if (TraceEnabled) - { - compositeStart = Stopwatch.GetTimestamp(); - } - + CompositionCommand command = compositions[i]; this.CompositeCoverage( configuration, compositeFrame, coverageHandle, Point.Empty, - brush, - graphicsOptions, - brushBounds); - - if (TraceEnabled) - { - double compositeMs = Stopwatch.GetElapsedTime(compositeStart).TotalMilliseconds; - Trace($"FillPath: composite={compositeMs:F3}ms"); - } - } - finally - { - if (openedCompositeSession) - { - this.EndCompositeSession(configuration, compositeFrame); - } + command.Brush, + command.GraphicsOptions, + command.BrushBounds); } } finally { + if (openedCompositeSession) + { + this.EndCompositeSession(configuration, compositeFrame); + } + this.ReleaseCoverage(coverageHandle); } } /// - /// Fills a rectangular region on the specified target region. + /// Determines whether this backend can composite coverage with the given brush/options. /// - /// - /// Rect fills are normalized through - /// so both APIs share the same coverage and composition paths. - /// - public void FillRegion( - Configuration configuration, - ICanvasFrame target, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle region) + public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) where TPixel : unmanaged, IPixel { - this.ThrowIfDisposed(); - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); Guard.NotNull(brush, nameof(brush)); + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler) || + !this.IsGPUReady) + { + return false; + } - if (!CanUseGpuSession()) + lock (this.gpuSync) { - this.fallbackBackend.FillRegion(configuration, target, brush, graphicsOptions, region); - return; + return this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _); } + } - Rectangle localTargetBounds = new(0, 0, target.Bounds.Width, target.Bounds.Height); - Rectangle clippedRegion = Rectangle.Intersect(localTargetBounds, region); - if (clippedRegion.Equals(Rectangle.Empty)) + private void FlushCompositionsFallback( + Configuration configuration, + ICanvasFrame target, + IReadOnlyList compositions) + where TPixel : unmanaged, IPixel + { + if (target.TryGetCpuRegion(out _)) { + this.fallbackBackend.FlushCompositions(configuration, target, compositions); return; } - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - RasterizerOptions rasterizerOptions = new( - clippedRegion, - IntersectionRule.NonZero, - rasterizationMode, - RasterizerSamplingOrigin.PixelBoundary); + Rectangle targetBounds = target.Bounds; + using Buffer2D stagingBuffer = configuration.MemoryAllocator.Allocate2D( + new Size(targetBounds.Width, targetBounds.Height), + AllocationOptions.Clean); + Buffer2DRegion stagingRegion = new(stagingBuffer, targetBounds); + CpuCanvasFrame stagingFrame = new(stagingRegion); + this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositions); - RectangularPolygon fillShape = new( - clippedRegion.X, - clippedRegion.Y, - clippedRegion.Width, - clippedRegion.Height); + if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || + nativeSurface is null || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability) || + surfaceCapability is null || + surfaceCapability.TargetTexture == 0) + { + throw new NotSupportedException( + "Fallback composition requires either a CPU destination region or a native WebGPU surface exposing a writable texture handle."); + } - this.FillPath( - configuration, - target, - fillShape, - brush, - graphicsOptions, - rasterizerOptions); + lock (this.gpuSync) + { + if (!this.QueueWriteTextureFromRegionLocked((Texture*)surfaceCapability.TargetTexture, stagingRegion)) + { + throw new NotSupportedException( + "Fallback composition could not upload to the native WebGPU target texture."); + } + } } - /// - /// Determines whether this backend can composite coverage with the given brush/options. - /// - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + private bool TryResolveGPUFlush(out CompositePixelRegistration pixelHandler) where TPixel : unmanaged, IPixel { - Guard.NotNull(brush, nameof(brush)); - - if (!TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler)) + pixelHandler = default; + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out pixelHandler) || + !this.IsGPUReady) { return false; } - return CanUseGpuComposite(graphicsOptions) - && WebGpuBrushData.TryCreate(brush, out _) - && this.TryEnsureGpuReady() - && this.HasCompositePipelineForTextureFormat(pixelHandler.TextureFormat); + lock (this.gpuSync) + { + return this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _); + } } /// @@ -533,7 +443,7 @@ public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions /// /// /// GPU preparation flattens path edges into local-interest coordinates, builds a tiled edge index, - /// and rasterizes the coverage texture. Unsupported scenarios delegate to fallback preparation. + /// and rasterizes the coverage texture. When GPU preparation is unavailable this returns an invalid handle. /// public DrawingCoverageHandle PrepareCoverage( IPath path, @@ -543,62 +453,35 @@ public DrawingCoverageHandle PrepareCoverage( { this.ThrowIfDisposed(); Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); + _ = allocator; + _ = preparationMode; this.PrepareCoverageCallCount++; Size size = rasterizerOptions.Interest.Size; - if (size.Width <= 0 || size.Height <= 0) - { - return default; - } - - if (preparationMode == CoveragePreparationMode.Fallback) - { - return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); - } - - if (!this.TryEnsureGpuReady()) - { - return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); - } - - if (!TryBuildCoverageTriangles( - path, - rasterizerOptions.Interest.Location, - rasterizerOptions.Interest.Size, - rasterizerOptions.SamplingOrigin, - out CoverageTriangleData coverageTriangleData)) - { - return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); - } Texture* coverageTexture = null; TextureView* coverageView = null; lock (this.gpuSync) { - if (!this.IsGpuReady || - this.webGpu is null || - this.device is null || - this.queue is null || - this.coverageStencilEvenOddPipeline is null || - this.coverageStencilNonZeroIncrementPipeline is null || - this.coverageStencilNonZeroDecrementPipeline is null || - this.coverageCoverPipeline is null || - !this.TryRasterizeCoverageTextureLocked( - coverageTriangleData, - in rasterizerOptions, - out coverageTexture, - out coverageView)) + WebGPURasterizer? rasterizer = this.coverageRasterizer; + if (rasterizer is null) + { + this.FallbackPrepareCoverageCallCount++; + return default; + } + + if (!rasterizer.TryCreateCoverageTexture(path, in rasterizerOptions, out coverageTexture, out coverageView)) { - return this.PrepareCoverageFallback(path, rasterizerOptions, allocator); + this.FallbackPrepareCoverageCallCount++; + return default; } } int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); CoverageEntry entry = new(size.Width, size.Height) { - GpuCoverageTexture = coverageTexture, - GpuCoverageView = coverageView + GPUCoverageTexture = coverageTexture, + GPUCoverageView = coverageView }; if (!this.preparedCoverage.TryAdd(handleId, entry)) @@ -612,39 +495,7 @@ this.coverageCoverPipeline is null || throw new InvalidOperationException("Failed to cache prepared coverage."); } - this.GpuPrepareCoverageCallCount++; - return new DrawingCoverageHandle(handleId); - } - - private DrawingCoverageHandle PrepareCoverageFallback( - IPath path, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator) - { - this.FallbackPrepareCoverageCallCount++; - DrawingCoverageHandle fallbackHandle = this.fallbackBackend.PrepareCoverage( - path, - rasterizerOptions, - allocator, - CoveragePreparationMode.Fallback); - if (!fallbackHandle.IsValid) - { - return default; - } - - Size size = rasterizerOptions.Interest.Size; - int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); - CoverageEntry entry = new(size.Width, size.Height) - { - FallbackCoverageHandle = fallbackHandle - }; - - if (!this.preparedCoverage.TryAdd(handleId, entry)) - { - this.fallbackBackend.ReleaseCoverage(fallbackHandle); - throw new InvalidOperationException("Failed to cache prepared fallback coverage."); - } - + this.GPUPrepareCoverageCallCount++; return new DrawingCoverageHandle(handleId); } @@ -652,9 +503,7 @@ private DrawingCoverageHandle PrepareCoverageFallback( /// Composes prepared coverage into a target region using the provided brush. /// /// - /// Handles prepared in fallback mode are always composed by the fallback backend. - /// Handles prepared in accelerated mode must be composed in accelerated mode. - /// Mixed-mode fallback is deliberately disabled to keep behavior explicit. + /// Coverage handles are GPU-prepared and must be composed on the active GPU session. /// public void CompositeCoverage( Configuration configuration, @@ -667,54 +516,14 @@ public void CompositeCoverage( where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - Guard.NotNull(brush, nameof(brush)); this.CompositeCoverageCallCount++; - if (!coverageHandle.IsValid) - { - return; - } - - if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out CoverageEntry? entry)) - { - throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); - } - - if (entry.IsFallback) - { - this.FallbackCompositeCoverageCallCount++; - this.fallbackBackend.CompositeCoverage( - configuration, - target, - entry.FallbackCoverageHandle, - sourceOffset, - brush, - graphicsOptions, - brushBounds); - return; - } - - if (!CanUseGpuComposite(graphicsOptions) || !this.TryEnsureGpuReady()) - { - throw new InvalidOperationException( - "Mixed-mode coverage composition is disabled. Coverage was prepared for accelerated composition, but the current composite settings are not GPU-supported."); - } - - if (!WebGpuBrushData.TryCreate(brush, out WebGpuBrushData brushData)) - { - throw new InvalidOperationException( - "Mixed-mode coverage composition is disabled. Coverage was prepared for accelerated composition, but the current composite settings are not GPU-supported."); - } - - if (!this.compositeSessionGpuActive || this.compositeSessionDepth <= 0) + if (!WebGPUBrushData.TryCreate(brush, brushBounds, out WebGPUBrushData brushData)) { - throw new InvalidOperationException( - "Accelerated coverage composition requires an active composite session."); + throw new InvalidOperationException("Unsupported brush for WebGPU composition."); } - if (!this.TryCompositeCoverageGpu( + if (!this.TryCompositeCoverageGPU( target, coverageHandle, sourceOffset, @@ -725,7 +534,7 @@ public void CompositeCoverage( "Accelerated coverage composition failed for a handle prepared for accelerated mode."); } - this.GpuCompositeCoverageCallCount++; + this.GPUCompositeCoverageCallCount++; } /// @@ -742,11 +551,6 @@ public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) Trace($"ReleaseCoverage: handle={coverageHandle.Value}"); if (this.preparedCoverage.TryRemove(coverageHandle.Value, out CoverageEntry? entry)) { - if (entry.IsFallback) - { - this.fallbackBackend.ReleaseCoverage(entry.FallbackCoverageHandle); - } - lock (this.gpuSync) { this.ReleaseCoverageTextureLocked(entry); @@ -774,65 +578,69 @@ public void Dispose() foreach (KeyValuePair kv in this.preparedCoverage) { - if (kv.Value.IsFallback) - { - this.fallbackBackend.ReleaseCoverage(kv.Value.FallbackCoverageHandle); - } - this.ReleaseCoverageTextureLocked(kv.Value); kv.Value.Dispose(); } this.preparedCoverage.Clear(); - this.ReleaseGpuResourcesLocked(); + this.ReleaseGPUResourcesLocked(); } this.isDisposed = true; Trace("Dispose: end"); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CanUseGpuComposite(in GraphicsOptions graphicsOptions) - where TPixel : unmanaged, IPixel - => HasCompositePixelHandler() - && graphicsOptions.AlphaCompositionMode == PixelAlphaCompositionMode.SrcOver - && graphicsOptions.ColorBlendingMode == PixelColorBlendingMode.Normal - && graphicsOptions.BlendPercentage > 0F; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CanUseGpuSession() + private bool ActivateCompositeSession( + ICanvasFrame target, + in CompositePixelRegistration pixelHandler) where TPixel : unmanaged, IPixel - => HasCompositePixelHandler(); + { + lock (this.gpuSync) + { + bool started = false; + if (target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) + { + started = this.BeginCompositeSessionCoreLocked( + cpuTarget, + pixelHandler.TextureFormat, + pixelHandler.PixelSizeInBytes); + } + else if (TryGetNativeSurfaceCapability(target, pixelHandler.TextureFormat, out WebGPUSurfaceCapability? nativeSurfaceCapability) && + nativeSurfaceCapability is not null && + this.BeginCompositeSurfaceSessionCoreLocked(target, nativeSurfaceCapability)) + { + started = true; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasCompositePixelHandler() - where TPixel : unmanaged, IPixel - => TryGetCompositePixelHandler(out _); + if (!started) + { + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetCompositePixelHandler(out CompositePixelRegistration pixelHandler) - where TPixel : unmanaged, IPixel - => CompositePixelHandlers.TryGetValue(typeof(TPixel), out pixelHandler); + this.compositeSessionGPUActive = true; + return true; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetNativeSurfaceCapability( ICanvasFrame target, TextureFormat expectedTargetFormat, - out WebGpuSurfaceCapability capability) + out WebGPUSurfaceCapability? capability) where TPixel : unmanaged, IPixel { if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || nativeSurface is null) { - capability = null!; + capability = null; return false; } - if (!nativeSurface.TryGetCapability(out WebGpuSurfaceCapability? surfaceCapability) || + if (!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability) || surfaceCapability is null || surfaceCapability.TargetTextureView == 0 || surfaceCapability.TargetFormat != expectedTargetFormat) { - capability = null!; + capability = null; return false; } @@ -840,147 +648,98 @@ surfaceCapability is null || return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HasCompositePipelineForTextureFormat(TextureFormat textureFormat) - { - if (textureFormat == TextureFormat.Undefined) - { - return false; - } - - lock (this.gpuSync) - { - return this.TryGetOrCreateCompositePipelineLocked(textureFormat, out _); - } - } - - /// - /// Ensures this instance has a ready-to-use GPU device/pipeline set. - /// - /// - /// Initialization is single-attempt per backend instance; subsequent calls are - /// cheap and return cached state. - /// - private bool TryEnsureGpuReady() - { - if (this.IsGpuReady) - { - return true; - } - - lock (this.gpuSync) - { - if (this.IsGpuReady) - { - return true; - } - - if (this.GpuInitializationAttempted) - { - return false; - } - - this.GpuInitializationAttempted = true; - this.LastGpuInitializationFailure = null; - this.IsGpuReady = this.TryInitializeGpuLocked(); - return this.IsGpuReady; - } - } - /// /// Performs one-time GPU initialization while is held. /// - private bool TryInitializeGpuLocked() + private bool TryInitializeGPULocked() { - Trace("TryInitializeGpuLocked: begin"); + Trace("TryInitializeGPULocked: begin"); try { - this.runtimeLease = WebGpuRuntime.Acquire(); - this.webGpu = this.runtimeLease.Api; + this.runtimeLease = WebGPURuntime.Acquire(); + this.webGPU = this.runtimeLease.Api; this.wgpuExtension = this.runtimeLease.WgpuExtension; - Trace($"TryInitializeGpuLocked: extension={(this.wgpuExtension is null ? "none" : "wgpu.h")}"); - this.instance = this.webGpu.CreateInstance((InstanceDescriptor*)null); + Trace($"TryInitializeGPULocked: extension={(this.wgpuExtension is null ? "none" : "wgpu.h")}"); + this.instance = this.webGPU.CreateInstance((InstanceDescriptor*)null); if (this.instance is null) { - this.LastGpuInitializationFailure = "WebGPU.CreateInstance returned null."; - Trace("TryInitializeGpuLocked: CreateInstance returned null"); + this.LastGPUInitializationFailure = "WebGPU.CreateInstance returned null."; + Trace("TryInitializeGPULocked: CreateInstance returned null"); return false; } - Trace("TryInitializeGpuLocked: created instance"); + Trace("TryInitializeGPULocked: created instance"); if (!this.TryRequestAdapterLocked(out this.adapter) || this.adapter is null) { - this.LastGpuInitializationFailure ??= "Failed to request WebGPU adapter."; - Trace($"TryInitializeGpuLocked: request adapter failed ({this.LastGpuInitializationFailure})"); + this.LastGPUInitializationFailure ??= "Failed to request WebGPU adapter."; + Trace($"TryInitializeGPULocked: request adapter failed ({this.LastGPUInitializationFailure})"); return false; } - Trace("TryInitializeGpuLocked: adapter acquired"); + Trace("TryInitializeGPULocked: adapter acquired"); if (!this.TryRequestDeviceLocked(out this.device) || this.device is null) { - this.LastGpuInitializationFailure ??= "Failed to request WebGPU device."; - Trace($"TryInitializeGpuLocked: request device failed ({this.LastGpuInitializationFailure})"); + this.LastGPUInitializationFailure ??= "Failed to request WebGPU device."; + Trace($"TryInitializeGPULocked: request device failed ({this.LastGPUInitializationFailure})"); return false; } - this.queue = this.webGpu.DeviceGetQueue(this.device); + this.queue = this.webGPU.DeviceGetQueue(this.device); if (this.queue is null) { - this.LastGpuInitializationFailure = "WebGPU.DeviceGetQueue returned null."; - Trace("TryInitializeGpuLocked: DeviceGetQueue returned null"); + this.LastGPUInitializationFailure = "WebGPU.DeviceGetQueue returned null."; + Trace("TryInitializeGPULocked: DeviceGetQueue returned null"); return false; } - Trace("TryInitializeGpuLocked: queue acquired"); + Trace("TryInitializeGPULocked: queue acquired"); if (!this.TryCreateCompositePipelineLocked()) { - this.LastGpuInitializationFailure = "Failed to create WebGPU composite pipeline."; - Trace("TryInitializeGpuLocked: composite pipeline creation failed"); + this.LastGPUInitializationFailure = "Failed to create WebGPU composite pipeline."; + Trace("TryInitializeGPULocked: composite pipeline creation failed"); return false; } - Trace("TryInitializeGpuLocked: composite pipeline ready"); - if (!this.TryCreateCoveragePipelineLocked()) + Trace("TryInitializeGPULocked: composite pipeline ready"); + this.coverageRasterizer = new WebGPURasterizer(this.webGPU, this.device, this.queue); + if (!this.coverageRasterizer.Initialize()) { - this.LastGpuInitializationFailure = "Failed to create WebGPU coverage pipeline."; - Trace("TryInitializeGpuLocked: coverage pipeline creation failed"); + this.LastGPUInitializationFailure = "Failed to create WebGPU coverage pipeline."; + Trace("TryInitializeGPULocked: coverage pipeline creation failed"); return false; } - Trace("TryInitializeGpuLocked: coverage pipeline ready"); + Trace("TryInitializeGPULocked: coverage pipeline ready"); return true; } catch (Exception ex) { - this.LastGpuInitializationFailure = $"WebGPU initialization threw: {ex.Message}"; - Trace($"TryInitializeGpuLocked: exception {ex}"); + this.LastGPUInitializationFailure = $"WebGPU initialization threw: {ex.Message}"; + Trace($"TryInitializeGPULocked: exception {ex}"); return false; } finally { - if (!this.IsGpuReady && + if (!this.IsGPUReady && (this.compositePipelineLayout is null || this.compositeBindGroupLayout is null || - this.coverageStencilEvenOddPipeline is null || - this.coverageStencilNonZeroIncrementPipeline is null || - this.coverageStencilNonZeroDecrementPipeline is null || - this.coverageCoverPipeline is null || - this.coveragePipelineLayout is null || + this.coverageRasterizer is null || + !this.coverageRasterizer.IsInitialized || this.device is null || this.queue is null)) { - this.LastGpuInitializationFailure ??= "WebGPU initialization left required resources unavailable."; - this.ReleaseGpuResourcesLocked(); + this.LastGPUInitializationFailure ??= "WebGPU initialization left required resources unavailable."; + this.ReleaseGPUResourcesLocked(); } - Trace($"TryInitializeGpuLocked: end ready={this.IsGpuReady} error={this.LastGpuInitializationFailure ?? ""}"); + Trace($"TryInitializeGPULocked: end ready={this.IsGPUReady} error={this.LastGPUInitializationFailure ?? ""}"); } } private bool TryRequestAdapterLocked(out Adapter* resultAdapter) { resultAdapter = null; - if (this.webGpu is null || this.instance is null) + if (this.webGPU is null || this.instance is null) { return false; } @@ -1003,10 +762,10 @@ void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* messagePtr PowerPreference = PowerPreference.HighPerformance }; - this.webGpu.InstanceRequestAdapter(this.instance, in options, callbackPtr, null); + this.webGPU.InstanceRequestAdapter(this.instance, in options, callbackPtr, null); if (!this.WaitForSignalLocked(callbackReady)) { - this.LastGpuInitializationFailure = "Timed out while waiting for WebGPU adapter request callback."; + this.LastGPUInitializationFailure = "Timed out while waiting for WebGPU adapter request callback."; Trace("TryRequestAdapterLocked: timeout waiting for callback"); return false; } @@ -1014,7 +773,7 @@ void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* messagePtr resultAdapter = callbackAdapter; if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) { - this.LastGpuInitializationFailure = $"WebGPU adapter request failed with status '{callbackStatus}'."; + this.LastGPUInitializationFailure = $"WebGPU adapter request failed with status '{callbackStatus}'."; Trace($"TryRequestAdapterLocked: callback status={callbackStatus} adapter={(nint)callbackAdapter:X}"); return false; } @@ -1025,7 +784,7 @@ void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* messagePtr private bool TryRequestDeviceLocked(out Device* resultDevice) { resultDevice = null; - if (this.webGpu is null || this.adapter is null) + if (this.webGPU is null || this.adapter is null) { return false; } @@ -1044,11 +803,11 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, v using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); DeviceDescriptor descriptor = default; - this.webGpu.AdapterRequestDevice(this.adapter, in descriptor, callbackPtr, null); + this.webGPU.AdapterRequestDevice(this.adapter, in descriptor, callbackPtr, null); if (!this.WaitForSignalLocked(callbackReady)) { - this.LastGpuInitializationFailure = "Timed out while waiting for WebGPU device request callback."; + this.LastGPUInitializationFailure = "Timed out while waiting for WebGPU device request callback."; Trace("TryRequestDeviceLocked: timeout waiting for callback"); return false; } @@ -1056,7 +815,7 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, v resultDevice = callbackDevice; if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) { - this.LastGpuInitializationFailure = $"WebGPU device request failed with status '{callbackStatus}'."; + this.LastGPUInitializationFailure = $"WebGPU device request failed with status '{callbackStatus}'."; Trace($"TryRequestDeviceLocked: callback status={callbackStatus} device={(nint)callbackDevice:X}"); return false; } @@ -1069,7 +828,7 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, v /// private bool TryCreateCompositePipelineLocked() { - if (this.webGpu is null || this.device is null) + if (this.webGPU is null || this.device is null) { return false; } @@ -1104,7 +863,7 @@ private bool TryCreateCompositePipelineLocked() Entries = layoutEntries }; - this.compositeBindGroupLayout = this.webGpu.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); + this.compositeBindGroupLayout = this.webGPU.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); if (this.compositeBindGroupLayout is null) { return false; @@ -1118,7 +877,7 @@ private bool TryCreateCompositePipelineLocked() BindGroupLayouts = bindGroupLayouts }; - this.compositePipelineLayout = this.webGpu.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); + this.compositePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); if (this.compositePipelineLayout is null) { return false; @@ -1139,7 +898,7 @@ private bool TryGetOrCreateCompositePipelineLocked(TextureFormat textureFormat, { pipeline = null; if (textureFormat == TextureFormat.Undefined || - this.webGpu is null || + this.webGPU is null || this.device is null || this.compositePipelineLayout is null) { @@ -1163,1015 +922,146 @@ this.device is null || nint cachedPipelineHandle = this.compositePipelines.GetOrAdd(textureFormat, createdPipelineHandle); if (cachedPipelineHandle != createdPipelineHandle) { - this.webGpu.RenderPipelineRelease(createdPipeline); - } - - pipeline = (RenderPipeline*)cachedPipelineHandle; - return pipeline is not null; - } - - private RenderPipeline* CreateCompositePipelineForFormatLocked(TextureFormat textureFormat) - { - if (this.webGpu is null || this.device is null) - { - return null; - } - - ShaderModule* shaderModule = null; - try - { - ReadOnlySpan shaderCode = CompositeCoverageShader.Code; - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct - { - SType = SType.ShaderModuleWgslDescriptor - }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.webGpu.DeviceCreateShaderModule(this.device, in shaderDescriptor); - } - - if (shaderModule is null) - { - return null; - } - - ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; - ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; - fixed (byte* vertexEntryPointPtr = vertexEntryPoint) - { - fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) - { - return this.CreateCompositePipelineLocked( - shaderModule, - vertexEntryPointPtr, - fragmentEntryPointPtr, - textureFormat); - } - } - } - finally - { - if (shaderModule is not null) - { - this.webGpu.ShaderModuleRelease(shaderModule); - } - } - } - - private RenderPipeline* CreateCompositePipelineLocked( - ShaderModule* shaderModule, - byte* vertexEntryPointPtr, - byte* fragmentEntryPointPtr, - TextureFormat textureFormat) - { - if (this.webGpu is null || this.device is null || this.compositePipelineLayout is null) - { - return null; - } - - VertexState vertexState = new() - { - Module = shaderModule, - EntryPoint = vertexEntryPointPtr, - BufferCount = 0, - Buffers = null - }; - - BlendState blendState = new() - { - Color = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - }, - Alpha = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - } - }; - - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; - colorTargets[0] = new ColorTargetState - { - Format = textureFormat, - Blend = &blendState, - WriteMask = ColorWriteMask.All - }; - - FragmentState fragmentState = new() - { - Module = shaderModule, - EntryPoint = fragmentEntryPointPtr, - TargetCount = 1, - Targets = colorTargets - }; - - RenderPipelineDescriptor pipelineDescriptor = new() - { - Layout = this.compositePipelineLayout, - Vertex = vertexState, - Primitive = new PrimitiveState - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }, - DepthStencil = null, - Multisample = new MultisampleState - { - Count = 1, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }, - Fragment = &fragmentState - }; - - return this.webGpu.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); - } - - /// - /// Creates the render pipeline used for coverage rasterization. - /// - private bool TryCreateCoveragePipelineLocked() - { - if (this.webGpu is null || this.device is null) - { - return false; - } - - PipelineLayoutDescriptor pipelineLayoutDescriptor = new() - { - BindGroupLayoutCount = 0, - BindGroupLayouts = null - }; - - this.coveragePipelineLayout = this.webGpu.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); - if (this.coveragePipelineLayout is null) - { - return false; - } - - ShaderModule* shaderModule = null; - try - { - ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct - { - SType = SType.ShaderModuleWgslDescriptor - }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.webGpu.DeviceCreateShaderModule(this.device, in shaderDescriptor); - } - - if (shaderModule is null) - { - return false; - } - - ReadOnlySpan stencilVertexEntryPoint = CoverageStencilVertexEntryPoint; - ReadOnlySpan stencilFragmentEntryPoint = CoverageStencilFragmentEntryPoint; - ReadOnlySpan coverVertexEntryPoint = CoverageCoverVertexEntryPoint; - ReadOnlySpan coverFragmentEntryPoint = CoverageCoverFragmentEntryPoint; - fixed (byte* stencilVertexEntryPointPtr = stencilVertexEntryPoint) - { - fixed (byte* stencilFragmentEntryPointPtr = stencilFragmentEntryPoint) - { - VertexAttribute* stencilVertexAttributes = stackalloc VertexAttribute[1]; - stencilVertexAttributes[0] = new VertexAttribute - { - Format = VertexFormat.Float32x2, - Offset = 0, - ShaderLocation = 0 - }; - - VertexBufferLayout* stencilVertexBuffers = stackalloc VertexBufferLayout[1]; - stencilVertexBuffers[0] = new VertexBufferLayout - { - ArrayStride = (ulong)Unsafe.SizeOf(), - StepMode = VertexStepMode.Vertex, - AttributeCount = 1, - Attributes = stencilVertexAttributes - }; - - VertexState stencilVertexState = new() - { - Module = shaderModule, - EntryPoint = stencilVertexEntryPointPtr, - BufferCount = 1, - Buffers = stencilVertexBuffers - }; - - ColorTargetState* stencilColorTargets = stackalloc ColorTargetState[1]; - stencilColorTargets[0] = new ColorTargetState - { - Format = TextureFormat.R8Unorm, - Blend = null, - WriteMask = ColorWriteMask.None - }; - - FragmentState stencilFragmentState = new() - { - Module = shaderModule, - EntryPoint = stencilFragmentEntryPointPtr, - TargetCount = 1, - Targets = stencilColorTargets - }; - - PrimitiveState primitiveState = new() - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }; - - MultisampleState multisampleState = new() - { - Count = CoverageSampleCount, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }; - - StencilFaceState evenOddStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.Invert - }; - - DepthStencilState evenOddDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = evenOddStencilFace, - StencilBack = evenOddStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor evenOddPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &evenOddDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilEvenOddPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in evenOddPipelineDescriptor); - if (this.coverageStencilEvenOddPipeline is null) - { - return false; - } - - StencilFaceState incrementStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.IncrementWrap - }; - - DepthStencilState incrementDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = incrementStencilFace, - StencilBack = incrementStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor incrementPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &incrementDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilNonZeroIncrementPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in incrementPipelineDescriptor); - if (this.coverageStencilNonZeroIncrementPipeline is null) - { - return false; - } - - StencilFaceState decrementStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.DecrementWrap - }; - - DepthStencilState decrementDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = decrementStencilFace, - StencilBack = decrementStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor decrementPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &decrementDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilNonZeroDecrementPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in decrementPipelineDescriptor); - if (this.coverageStencilNonZeroDecrementPipeline is null) - { - return false; - } - } - } - - fixed (byte* coverVertexEntryPointPtr = coverVertexEntryPoint) - { - fixed (byte* coverFragmentEntryPointPtr = coverFragmentEntryPoint) - { - VertexState coverVertexState = new() - { - Module = shaderModule, - EntryPoint = coverVertexEntryPointPtr, - BufferCount = 0, - Buffers = null - }; - - ColorTargetState* coverColorTargets = stackalloc ColorTargetState[1]; - coverColorTargets[0] = new ColorTargetState - { - Format = TextureFormat.R8Unorm, - Blend = null, - WriteMask = ColorWriteMask.Red - }; - - FragmentState coverFragmentState = new() - { - Module = shaderModule, - EntryPoint = coverFragmentEntryPointPtr, - TargetCount = 1, - Targets = coverColorTargets - }; - - StencilFaceState coverStencilFace = new() - { - Compare = CompareFunction.NotEqual, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.Keep - }; - - DepthStencilState coverDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = coverStencilFace, - StencilBack = coverStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = 0, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor coverPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = coverVertexState, - Primitive = new PrimitiveState - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }, - DepthStencil = &coverDepthStencilState, - Multisample = new MultisampleState - { - Count = CoverageSampleCount, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }, - Fragment = &coverFragmentState - }; - - this.coverageCoverPipeline = this.webGpu.DeviceCreateRenderPipeline(this.device, in coverPipelineDescriptor); - } - } - - return this.coverageCoverPipeline is not null; - } - finally - { - if (shaderModule is not null) - { - this.webGpu.ShaderModuleRelease(shaderModule); - } - } - } - - private bool TryEnsureCoverageScratchTargetsLocked( - int width, - int height, - out TextureView* multisampleCoverageView, - out TextureView* stencilView) - { - multisampleCoverageView = null; - stencilView = null; - - if (this.webGpu is null || this.device is null || width <= 0 || height <= 0) - { - return false; - } - - if (this.coverageScratchMultisampleView is not null && - this.coverageScratchStencilView is not null && - this.coverageScratchWidth == width && - this.coverageScratchHeight == height) - { - multisampleCoverageView = this.coverageScratchMultisampleView; - stencilView = this.coverageScratchStencilView; - return true; - } - - this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); - this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); - this.ReleaseTextureViewLocked(this.coverageScratchStencilView); - this.ReleaseTextureLocked(this.coverageScratchStencilTexture); - this.coverageScratchMultisampleView = null; - this.coverageScratchMultisampleTexture = null; - this.coverageScratchStencilView = null; - this.coverageScratchStencilTexture = null; - this.coverageScratchWidth = 0; - this.coverageScratchHeight = 0; - - TextureDescriptor multisampleCoverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - Texture* createdMultisampleCoverageTexture = - this.webGpu.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); - if (createdMultisampleCoverageTexture is null) - { - return false; - } - - TextureViewDescriptor coverageViewDescriptor = new() - { - Format = TextureFormat.R8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* createdMultisampleCoverageView = this.webGpu.TextureCreateView(createdMultisampleCoverageTexture, in coverageViewDescriptor); - if (createdMultisampleCoverageView is null) - { - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - TextureDescriptor stencilTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.Depth24PlusStencil8, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - Texture* createdStencilTexture = this.webGpu.DeviceCreateTexture(this.device, in stencilTextureDescriptor); - if (createdStencilTexture is null) - { - this.ReleaseTextureViewLocked(createdMultisampleCoverageView); - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - TextureViewDescriptor stencilViewDescriptor = new() - { - Format = TextureFormat.Depth24PlusStencil8, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* createdStencilView = this.webGpu.TextureCreateView(createdStencilTexture, in stencilViewDescriptor); - if (createdStencilView is null) - { - this.ReleaseTextureLocked(createdStencilTexture); - this.ReleaseTextureViewLocked(createdMultisampleCoverageView); - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - this.coverageScratchMultisampleTexture = createdMultisampleCoverageTexture; - this.coverageScratchMultisampleView = createdMultisampleCoverageView; - this.coverageScratchStencilTexture = createdStencilTexture; - this.coverageScratchStencilView = createdStencilView; - this.coverageScratchWidth = width; - this.coverageScratchHeight = height; - - multisampleCoverageView = createdMultisampleCoverageView; - stencilView = createdStencilView; - return true; - } - - private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) - { - if (this.webGpu is null || this.device is null || requiredByteCount == 0) - { - return false; - } - - if (this.coverageScratchVertexBuffer is not null && - this.coverageScratchVertexCapacityBytes >= requiredByteCount) - { - return true; - } - - this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); - this.coverageScratchVertexBuffer = null; - this.coverageScratchVertexCapacityBytes = 0; - - BufferDescriptor vertexBufferDescriptor = new() - { - Usage = BufferUsage.Vertex | BufferUsage.CopyDst, - Size = requiredByteCount - }; - - WgpuBuffer* createdVertexBuffer = this.webGpu.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); - if (createdVertexBuffer is null) - { - return false; - } - - this.coverageScratchVertexBuffer = createdVertexBuffer; - this.coverageScratchVertexCapacityBytes = requiredByteCount; - return true; - } - - /// - /// Rasterizes edge triangles through a stencil-and-cover pass into an R8Unorm texture. - /// - private bool TryRasterizeCoverageTextureLocked( - in CoverageTriangleData coverageTriangleData, - in RasterizerOptions rasterizerOptions, - out Texture* coverageTexture, - out TextureView* coverageView) - { - Trace($"TryRasterizeCoverageTextureLocked: begin triangles={coverageTriangleData.TotalVertexCount / 3} size={rasterizerOptions.Interest.Width}x{rasterizerOptions.Interest.Height}"); - coverageTexture = null; - coverageView = null; - - if (this.webGpu is null || - this.device is null || - this.queue is null || - this.coverageStencilEvenOddPipeline is null || - this.coverageStencilNonZeroIncrementPipeline is null || - this.coverageStencilNonZeroDecrementPipeline is null || - this.coverageCoverPipeline is null || - coverageTriangleData.TotalVertexCount == 0 || - rasterizerOptions.Interest.Width <= 0 || - rasterizerOptions.Interest.Height <= 0) - { - return false; - } - - Texture* createdCoverageTexture = null; - TextureView* createdCoverageView = null; - CommandEncoder* commandEncoder = null; - RenderPassEncoder* passEncoder = null; - CommandBuffer* commandBuffer = null; - bool success = false; - try - { - if (!this.TryEnsureCoverageScratchTargetsLocked( - rasterizerOptions.Interest.Width, - rasterizerOptions.Interest.Height, - out TextureView* multisampleCoverageView, - out TextureView* stencilView)) - { - return false; - } - - TextureDescriptor coverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = 1 - }; - - createdCoverageTexture = this.webGpu.DeviceCreateTexture(this.device, in coverageTextureDescriptor); - if (createdCoverageTexture is null) - { - return false; - } - - TextureViewDescriptor coverageViewDescriptor = new() - { - Format = TextureFormat.R8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - createdCoverageView = this.webGpu.TextureCreateView(createdCoverageTexture, in coverageViewDescriptor); - if (createdCoverageView is null) - { - return false; - } - - ulong vertexByteCount = checked(coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); - if (!this.TryEnsureCoverageScratchVertexBufferLocked(vertexByteCount) || this.coverageScratchVertexBuffer is null) - { - return false; - } - - fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) - { - this.webGpu.QueueWriteBuffer(this.queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); - } - - CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); - if (commandEncoder is null) - { - return false; - } - - RenderPassColorAttachment colorAttachment = new() - { - View = multisampleCoverageView, - ResolveTarget = createdCoverageView, - LoadOp = LoadOp.Clear, - StoreOp = StoreOp.Discard, - ClearValue = default - }; - - RenderPassDepthStencilAttachment depthStencilAttachment = new() - { - View = stencilView, - DepthLoadOp = LoadOp.Clear, - DepthStoreOp = StoreOp.Discard, - DepthClearValue = 1F, - DepthReadOnly = false, - StencilLoadOp = LoadOp.Clear, - StencilStoreOp = StoreOp.Discard, - StencilClearValue = 0, - StencilReadOnly = false - }; - - RenderPassDescriptor renderPassDescriptor = new() - { - ColorAttachmentCount = 1, - ColorAttachments = &colorAttachment, - DepthStencilAttachment = &depthStencilAttachment - }; - - passEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); - if (passEncoder is null) - { - return false; - } - - this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); - this.webGpu.RenderPassEncoderSetVertexBuffer(passEncoder, 0, this.coverageScratchVertexBuffer, 0, vertexByteCount); - if (rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd) - { - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilEvenOddPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, coverageTriangleData.TotalVertexCount, 1, 0, 0); - } - else - { - if (coverageTriangleData.IncrementVertexCount > 0) - { - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, coverageTriangleData.IncrementVertexCount, 1, 0, 0); - } - - if (coverageTriangleData.DecrementVertexCount > 0) - { - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); - this.webGpu.RenderPassEncoderDraw( - passEncoder, - coverageTriangleData.DecrementVertexCount, - 1, - coverageTriangleData.IncrementVertexCount, - 0); - } - } - - this.webGpu.RenderPassEncoderSetStencilReference(passEncoder, 0); - this.webGpu.RenderPassEncoderSetPipeline(passEncoder, this.coverageCoverPipeline); - this.webGpu.RenderPassEncoderDraw(passEncoder, CoverageCoverVertexCount, 1, 0, 0); - - this.webGpu.RenderPassEncoderEnd(passEncoder); - this.webGpu.RenderPassEncoderRelease(passEncoder); - passEncoder = null; - - CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); - if (commandBuffer is null) - { - return false; - } - - this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - - this.webGpu.CommandBufferRelease(commandBuffer); - commandBuffer = null; - coverageTexture = createdCoverageTexture; - coverageView = createdCoverageView; - createdCoverageTexture = null; - createdCoverageView = null; - success = true; - Trace("TryRasterizeCoverageTextureLocked: submitted"); - return true; - } - finally - { - if (passEncoder is not null) - { - this.webGpu.RenderPassEncoderRelease(passEncoder); - } - - if (commandBuffer is not null) - { - this.webGpu.CommandBufferRelease(commandBuffer); - } - - if (commandEncoder is not null) - { - this.webGpu.CommandEncoderRelease(commandEncoder); - } - - if (!success) - { - this.ReleaseTextureViewLocked(createdCoverageView); - this.ReleaseTextureLocked(createdCoverageTexture); - } - } - } - - /// - /// Flattens a path into local-interest coordinates and converts each non-horizontal edge - /// into a trapezoid (two triangles) anchored at a left-side sentinel X. - /// - private static bool TryBuildCoverageTriangles( - IPath path, - Point interestLocation, - Size interestSize, - RasterizerSamplingOrigin samplingOrigin, - out CoverageTriangleData coverageTriangleData) - { - coverageTriangleData = default; - if (interestSize.Width <= 0 || interestSize.Height <= 0) - { - return false; - } - - float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelBoundary ? 0.5F : 0F; - float offsetX = sampleShift - interestLocation.X; - float offsetY = sampleShift - interestLocation.Y; - - List segments = []; - float minX = float.PositiveInfinity; - - foreach (ISimplePath simplePath in path.Flatten()) - { - ReadOnlySpan points = simplePath.Points.Span; - if (points.Length < 2) - { - continue; - } - - for (int i = 1; i < points.Length; i++) - { - AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX); - } - - if (simplePath.IsClosed) - { - AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX); - } + this.webGPU.RenderPipelineRelease(createdPipeline); } - if (segments.Count == 0 || !float.IsFinite(minX)) + pipeline = (RenderPipeline*)cachedPipelineHandle; + return pipeline is not null; + } + + private RenderPipeline* CreateCompositePipelineForFormatLocked(TextureFormat textureFormat) + { + if (this.webGPU is null || this.device is null) { - return false; + return null; } - int incrementEdgeCount = 0; - int decrementEdgeCount = 0; - foreach (CoverageSegment segment in segments) + ShaderModule* shaderModule = null; + try { - if (segment.FromY == segment.ToY) + ReadOnlySpan shaderCode = CompositeCoverageShader.Code; + fixed (byte* shaderCodePtr = shaderCode) { - continue; - } + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct + { + SType = SType.ShaderModuleWgslDescriptor + }, + Code = shaderCodePtr + }; - if (segment.ToY > segment.FromY) - { - incrementEdgeCount++; + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + shaderModule = this.webGPU.DeviceCreateShaderModule(this.device, in shaderDescriptor); } - else + + if (shaderModule is null) { - decrementEdgeCount++; + return null; } - } - - int totalEdgeCount = incrementEdgeCount + decrementEdgeCount; - if (totalEdgeCount == 0) - { - return false; - } - - float sentinelX = minX - 1F; - float widthScale = 2F / interestSize.Width; - float heightScale = 2F / interestSize.Height; - int incrementVertexCount = checked(incrementEdgeCount * 6); - int decrementVertexCount = checked(decrementEdgeCount * 6); - StencilVertex[] vertices = new StencilVertex[checked(incrementVertexCount + decrementVertexCount)]; - int vertexIndex = 0; - foreach (CoverageSegment segment in segments) - { - if (segment.ToY <= segment.FromY) + ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; + ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) { - continue; + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + { + return this.CreateCompositePipelineLocked( + shaderModule, + vertexEntryPointPtr, + fragmentEntryPointPtr, + textureFormat); + } } - - AppendCoverageEdgeQuad( - vertices, - ref vertexIndex, - sentinelX, - segment.FromX, - segment.FromY, - segment.ToX, - segment.ToY, - widthScale, - heightScale); } - - int decrementStartIndex = incrementVertexCount; - vertexIndex = decrementStartIndex; - foreach (CoverageSegment segment in segments) + finally { - if (segment.ToY >= segment.FromY) + if (shaderModule is not null) { - continue; + this.webGPU.ShaderModuleRelease(shaderModule); } - - AppendCoverageEdgeQuad( - vertices, - ref vertexIndex, - sentinelX, - segment.FromX, - segment.FromY, - segment.ToX, - segment.ToY, - widthScale, - heightScale); } - - coverageTriangleData = new CoverageTriangleData( - vertices, - (uint)incrementVertexCount, - (uint)decrementVertexCount); - return true; } - private static void AddCoverageSegment( - PointF from, - PointF to, - float offsetX, - float offsetY, - List destination, - ref float minX) + private RenderPipeline* CreateCompositePipelineLocked( + ShaderModule* shaderModule, + byte* vertexEntryPointPtr, + byte* fragmentEntryPointPtr, + TextureFormat textureFormat) { - if (from.Equals(to)) + if (this.webGPU is null || this.device is null || this.compositePipelineLayout is null) { - return; + return null; } - if (!float.IsFinite(from.X) || - !float.IsFinite(from.Y) || - !float.IsFinite(to.X) || - !float.IsFinite(to.Y)) + VertexState vertexState = new() { - return; - } + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; - float fromX = from.X + offsetX; - float fromY = from.Y + offsetY; - float toX = to.X + offsetX; - float toY = to.Y + offsetY; + BlendState blendState = new() + { + Color = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + }, + Alpha = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + } + }; - destination.Add(new CoverageSegment(fromX, fromY, toX, toY)); - minX = MathF.Min(minX, MathF.Min(fromX, toX)); - } + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = textureFormat, + Blend = &blendState, + WriteMask = ColorWriteMask.All + }; - private static void AppendCoverageEdgeQuad( - StencilVertex[] destination, - ref int destinationIndex, - float sentinelX, - float fromX, - float fromY, - float toX, - float toY, - float widthScale, - float heightScale) - { - StencilVertex a = ToStencilVertex(sentinelX, fromY, widthScale, heightScale); - StencilVertex b = ToStencilVertex(fromX, fromY, widthScale, heightScale); - StencilVertex c = ToStencilVertex(toX, toY, widthScale, heightScale); - StencilVertex d = ToStencilVertex(sentinelX, toY, widthScale, heightScale); - - destination[destinationIndex++] = a; - destination[destinationIndex++] = b; - destination[destinationIndex++] = c; - destination[destinationIndex++] = a; - destination[destinationIndex++] = c; - destination[destinationIndex++] = d; - } + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static StencilVertex ToStencilVertex(float x, float y, float widthScale, float heightScale) - => new() + RenderPipelineDescriptor pipelineDescriptor = new() { - X = (x * widthScale) - 1F, - Y = 1F - (y * heightScale) + Layout = this.compositePipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState }; + return this.webGPU.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); + } + private bool WaitForSignalLocked(ManualResetEventSlim signal) { Stopwatch timer = Stopwatch.StartNew(); @@ -2189,9 +1079,9 @@ private bool WaitForSignalLocked(ManualResetEventSlim signal) continue; } - if (this.instance is not null && this.webGpu is not null) + if (this.instance is not null && this.webGPU is not null) { - this.webGpu.InstanceProcessEvents(this.instance); + this.webGPU.InstanceProcessEvents(this.instance); } if (!signal.IsSet) @@ -2210,14 +1100,16 @@ private bool WaitForSignalLocked(ManualResetEventSlim signal) return true; } - private bool TryQueueWriteTextureFromRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) + private bool QueueWriteTextureFromRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) where TPixel : unmanaged { - if (this.webGpu is null || this.queue is null || destinationTexture is null) + if (!this.TryGetGPUState(out GPUState gpuState)) { return false; } + WebGPU api = gpuState.Api; + Queue* queue = gpuState.Queue; int pixelSizeInBytes = Unsafe.SizeOf(); ImageCopyTexture destination = new() { @@ -2249,7 +1141,7 @@ private bool TryQueueWriteTextureFromRegionLocked(Texture* destinationTe Span firstRow = sourceRegion.DangerousGetRowSpan(0); fixed (TPixel* uploadPtr = firstRow) { - this.webGpu.QueueWriteTexture(this.queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); + api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); } return true; @@ -2276,7 +1168,7 @@ private bool TryQueueWriteTextureFromRegionLocked(Texture* destinationTe fixed (byte* uploadPtr = packedData) { - this.webGpu.QueueWriteTexture(this.queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); + api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); } return true; @@ -2294,38 +1186,25 @@ private bool TryQueueWriteTextureFromRegionLocked(Texture* destinationTe /// /// Ensures session resources for the target size, then uploads target pixels once. /// - private bool TryBeginCompositeSessionCoreLocked( + private bool BeginCompositeSessionCoreLocked( Buffer2DRegion target, TextureFormat textureFormat, int pixelSizeInBytes) where TPixel : unmanaged { - if (!this.IsGpuReady || - this.webGpu is null || - this.device is null || - this.queue is null || - pixelSizeInBytes <= 0 || - target.Width <= 0 || - target.Height <= 0) - { - return false; - } - - if (!this.TryEnsureCompositeSessionResourcesLocked(target.Width, target.Height, textureFormat, pixelSizeInBytes) || + if (!this.EnsureCompositeSessionResourcesLocked(target.Width, target.Height, textureFormat, pixelSizeInBytes) || this.compositeSessionTargetTexture is null) { return false; } this.ResetCompositeSessionStateLocked(); - if (!this.TryQueueWriteTextureFromRegionLocked(this.compositeSessionTargetTexture, target)) + if (!this.QueueWriteTextureFromRegionLocked(this.compositeSessionTargetTexture, target)) { return false; } this.compositeSessionTargetRectangle = target.Rectangle; - this.compositeSessionTargetWidth = target.Width; - this.compositeSessionTargetHeight = target.Height; this.compositeSessionRequiresReadback = true; this.compositeSessionOwnsTargetView = true; this.compositeSessionUniformWriteOffset = 0; @@ -2333,28 +1212,14 @@ this.queue is null || return true; } - private bool TryBeginCompositeSurfaceSessionCoreLocked( + private bool BeginCompositeSurfaceSessionCoreLocked( ICanvasFrame target, - WebGpuSurfaceCapability nativeSurfaceCapability) + WebGPUSurfaceCapability nativeSurfaceCapability) where TPixel : unmanaged, IPixel { - if (!this.IsGpuReady || - this.webGpu is null || - this.device is null || - this.queue is null || - nativeSurfaceCapability.TargetTextureView == 0 || - nativeSurfaceCapability.Device == 0 || - nativeSurfaceCapability.Queue == 0 || + if (nativeSurfaceCapability.TargetTextureView == 0 || target.Bounds.Width <= 0 || - target.Bounds.Height <= 0 || - target.Bounds.X < 0 || - target.Bounds.Y < 0) - { - return false; - } - - if (nativeSurfaceCapability.Device != (nint)this.device || - nativeSurfaceCapability.Queue != (nint)this.queue) + target.Bounds.Height <= 0) { return false; } @@ -2389,25 +1254,18 @@ this.queue is null || this.compositeSessionOwnsTargetView = false; this.compositeSessionRequiresReadback = false; this.compositeSessionTargetRectangle = target.Bounds; - this.compositeSessionTargetWidth = target.Bounds.Width; - this.compositeSessionTargetHeight = target.Bounds.Height; this.compositeSessionUniformWriteOffset = 0; this.compositeSessionDirty = false; return this.TryEnsureCompositeSessionUniformBufferLocked(); } - private bool TryEnsureCompositeSessionResourcesLocked( + private bool EnsureCompositeSessionResourcesLocked( int width, int height, TextureFormat textureFormat, int pixelSizeInBytes) { - if (!this.IsGpuReady || - this.webGpu is null || - this.device is null || - pixelSizeInBytes <= 0 || - width <= 0 || - height <= 0) + if (!this.TryGetGPUState(out GPUState gpuState)) { return false; } @@ -2439,7 +1297,7 @@ this.compositeSessionUniformBuffer is not null && SampleCount = 1 }; - Texture* targetTexture = this.webGpu.DeviceCreateTexture(this.device, in targetTextureDescriptor); + Texture* targetTexture = gpuState.Api.DeviceCreateTexture(gpuState.Device, in targetTextureDescriptor); if (targetTexture is null) { return false; @@ -2456,7 +1314,7 @@ this.compositeSessionUniformBuffer is not null && Aspect = TextureAspect.All }; - TextureView* targetView = this.webGpu.TextureCreateView(targetTexture, in targetViewDescriptor); + TextureView* targetView = gpuState.Api.TextureCreateView(targetTexture, in targetViewDescriptor); if (targetView is null) { this.ReleaseTextureLocked(targetTexture); @@ -2469,7 +1327,7 @@ this.compositeSessionUniformBuffer is not null && Size = readbackByteCount }; - WgpuBuffer* readbackBuffer = this.webGpu.DeviceCreateBuffer(this.device, in readbackBufferDescriptor); + WgpuBuffer* readbackBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in readbackBufferDescriptor); if (readbackBuffer is null) { this.ReleaseTextureViewLocked(targetView); @@ -2483,7 +1341,7 @@ this.compositeSessionUniformBuffer is not null && Size = CompositeUniformBufferSize }; - WgpuBuffer* uniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); + WgpuBuffer* uniformBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in uniformBufferDescriptor); if (uniformBuffer is null) { this.ReleaseBufferLocked(readbackBuffer); @@ -2514,7 +1372,7 @@ private bool TryEnsureCompositeSessionUniformBufferLocked() return true; } - if (this.webGpu is null || this.device is null) + if (!this.TryGetGPUState(out GPUState gpuState)) { return false; } @@ -2525,7 +1383,7 @@ private bool TryEnsureCompositeSessionUniformBufferLocked() Size = CompositeUniformBufferSize }; - this.compositeSessionUniformBuffer = this.webGpu.DeviceCreateBuffer(this.device, in uniformBufferDescriptor); + this.compositeSessionUniformBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in uniformBufferDescriptor); return this.compositeSessionUniformBuffer is not null; } @@ -2535,21 +1393,25 @@ private bool TryEnsureCompositeSessionUniformBufferLocked() private bool TryFlushCompositeSessionLocked(Buffer2DRegion target) where TPixel : unmanaged, IPixel { + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return false; + } + Trace("TryFlushCompositeSessionLocked: begin"); - if (this.webGpu is null || - this.device is null || - this.queue is null || - this.compositeSessionTargetTexture is null || + int targetWidth = this.compositeSessionTargetRectangle.Width; + int targetHeight = this.compositeSessionTargetRectangle.Height; + if (this.compositeSessionTargetTexture is null || this.compositeSessionReadbackBuffer is null || - this.compositeSessionTargetWidth <= 0 || - this.compositeSessionTargetHeight <= 0 || + targetWidth <= 0 || + targetHeight <= 0 || this.compositeSessionReadbackByteCount == 0 || this.compositeSessionReadbackBytesPerRow == 0) { return false; } - if (target.Width != this.compositeSessionTargetWidth || target.Height != this.compositeSessionTargetHeight) + if (target.Width != targetWidth || target.Height != targetHeight) { return false; } @@ -2564,7 +1426,7 @@ this.compositeSessionReadbackBuffer is null || if (commandEncoder is null) { CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + commandEncoder = gpuState.Api.DeviceCreateCommandEncoder(gpuState.Device, in commandEncoderDescriptor); if (commandEncoder is null) { return false; @@ -2586,15 +1448,15 @@ this.compositeSessionReadbackBuffer is null || { Offset = 0, BytesPerRow = this.compositeSessionReadbackBytesPerRow, - RowsPerImage = (uint)this.compositeSessionTargetHeight + RowsPerImage = (uint)targetHeight } }; - Extent3D copySize = new((uint)this.compositeSessionTargetWidth, (uint)this.compositeSessionTargetHeight, 1); - this.webGpu.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); + Extent3D copySize = new((uint)targetWidth, (uint)targetHeight, 1); + gpuState.Api.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + commandBuffer = gpuState.Api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); if (commandBuffer is null) { return false; @@ -2602,8 +1464,8 @@ this.compositeSessionReadbackBuffer is null || this.compositeSessionCommandEncoder = null; - this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - this.webGpu.CommandBufferRelease(commandBuffer); + gpuState.Api.QueueSubmit(gpuState.Queue, 1, ref commandBuffer); + gpuState.Api.CommandBufferRelease(commandBuffer); commandBuffer = null; bool readbackSuccess = this.TryReadBackBufferToRegionLocked( @@ -2629,7 +1491,7 @@ this.compositeSessionReadbackBuffer is null || if (commandBuffer is not null) { - this.webGpu.CommandBufferRelease(commandBuffer); + gpuState.Api.CommandBufferRelease(commandBuffer); } if (commandEncoder is not null) @@ -2639,14 +1501,14 @@ this.compositeSessionReadbackBuffer is null || this.compositeSessionCommandEncoder = null; } - this.webGpu.CommandEncoderRelease(commandEncoder); + gpuState.Api.CommandEncoderRelease(commandEncoder); } } } private bool TrySubmitCompositeSessionLocked() { - if (this.webGpu is null || this.device is null || this.queue is null) + if (!this.TryGetGPUState(out GPUState gpuState)) { return false; } @@ -2663,15 +1525,15 @@ private bool TrySubmitCompositeSessionLocked() } CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGpu.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + commandBuffer = gpuState.Api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); if (commandBuffer is null) { return false; } this.compositeSessionCommandEncoder = null; - this.webGpu.QueueSubmit(this.queue, 1, ref commandBuffer); - this.webGpu.CommandBufferRelease(commandBuffer); + gpuState.Api.QueueSubmit(gpuState.Queue, 1, ref commandBuffer); + gpuState.Api.CommandBufferRelease(commandBuffer); commandBuffer = null; return true; } @@ -2679,12 +1541,12 @@ private bool TrySubmitCompositeSessionLocked() { if (commandBuffer is not null) { - this.webGpu.CommandBufferRelease(commandBuffer); + gpuState.Api.CommandBufferRelease(commandBuffer); } if (commandEncoder is not null) { - this.webGpu.CommandEncoderRelease(commandEncoder); + gpuState.Api.CommandEncoderRelease(commandEncoder); } } } @@ -2693,30 +1555,29 @@ private void ResetCompositeSessionStateLocked() { this.TryCloseCompositeSessionPassLocked(); - if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) + if (this.compositeSessionCommandEncoder is not null && this.webGPU is not null) { - this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); + this.webGPU.CommandEncoderRelease(this.compositeSessionCommandEncoder); this.compositeSessionCommandEncoder = null; } this.compositeSessionTargetRectangle = default; - this.compositeSessionTargetWidth = 0; - this.compositeSessionTargetHeight = 0; this.compositeSessionRequiresReadback = false; this.compositeSessionDirty = false; + this.compositeSessionCommands.Clear(); } private void ReleaseCompositeSessionResourcesLocked() { - if (this.compositeSessionPassEncoder is not null && this.webGpu is not null) + if (this.compositeSessionPassEncoder is not null && this.webGPU is not null) { - this.webGpu.RenderPassEncoderRelease(this.compositeSessionPassEncoder); + this.webGPU.RenderPassEncoderRelease(this.compositeSessionPassEncoder); this.compositeSessionPassEncoder = null; } - if (this.compositeSessionCommandEncoder is not null && this.webGpu is not null) + if (this.compositeSessionCommandEncoder is not null && this.webGPU is not null) { - this.webGpu.CommandEncoderRelease(this.compositeSessionCommandEncoder); + this.webGPU.CommandEncoderRelease(this.compositeSessionCommandEncoder); this.compositeSessionCommandEncoder = null; } @@ -2741,31 +1602,22 @@ private void ReleaseCompositeSessionResourcesLocked() this.compositeSessionResourceWidth = 0; this.compositeSessionResourceHeight = 0; this.compositeSessionResourceTextureFormat = TextureFormat.Undefined; + this.compositeSessionCommands.Clear(); } - private bool TryCompositeCoverageGpu( + private bool TryCompositeCoverageGPU( ICanvasFrame target, DrawingCoverageHandle coverageHandle, Point sourceOffset, - WebGpuBrushData brushData, + WebGPUBrushData brushData, float blendPercentage) where TPixel : unmanaged, IPixel { - if (!coverageHandle.IsValid) - { - return true; - } - if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out CoverageEntry? entry)) { throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); } - if (entry.IsFallback) - { - return false; - } - if (target.Bounds.Width <= 0 || target.Bounds.Height <= 0) { return true; @@ -2785,8 +1637,9 @@ private bool TryCompositeCoverageGpu( lock (this.gpuSync) { - if (!this.IsGpuReady || this.webGpu is null || this.device is null || this.queue is null || - this.compositeBindGroupLayout is null) + if (!this.compositeSessionGPUActive || + this.compositeSessionDepth <= 0 || + this.compositeSessionTargetView is null) { return false; } @@ -2796,62 +1649,88 @@ private bool TryCompositeCoverageGpu( return false; } - if (this.compositeSessionGpuActive && - this.compositeSessionTargetView is not null) + int sessionTargetWidth = this.compositeSessionTargetRectangle.Width; + int sessionTargetHeight = this.compositeSessionTargetRectangle.Height; + int destinationX = target.Bounds.X - this.compositeSessionTargetRectangle.X; + int destinationY = target.Bounds.Y - this.compositeSessionTargetRectangle.Y; + if ((uint)destinationX >= (uint)sessionTargetWidth || + (uint)destinationY >= (uint)sessionTargetHeight) { - RenderPipeline* compositePipeline = this.GetCompositeSessionPipelineLocked(); - if (compositePipeline is null) - { - return false; - } + return false; + } - int destinationX = target.Bounds.X - this.compositeSessionTargetRectangle.X; - int destinationY = target.Bounds.Y - this.compositeSessionTargetRectangle.Y; - if ((uint)destinationX >= (uint)this.compositeSessionTargetWidth || - (uint)destinationY >= (uint)this.compositeSessionTargetHeight) - { - return false; - } + int sessionCompositeWidth = Math.Min(compositeWidth, sessionTargetWidth - destinationX); + int sessionCompositeHeight = Math.Min(compositeHeight, sessionTargetHeight - destinationY); + if (sessionCompositeWidth <= 0 || sessionCompositeHeight <= 0) + { + return true; + } - int sessionCompositeWidth = Math.Min(compositeWidth, this.compositeSessionTargetWidth - destinationX); - int sessionCompositeHeight = Math.Min(compositeHeight, this.compositeSessionTargetHeight - destinationY); - if (sessionCompositeWidth <= 0 || sessionCompositeHeight <= 0) - { - return true; - } + this.compositeSessionCommands.Add(new GPUCompositeCommand( + coverageHandle.Value, + sourceOffset, + brushData, + blendPercentage, + destinationX, + destinationY, + sessionCompositeWidth, + sessionCompositeHeight)); + this.compositeSessionDirty = true; + return true; + } + } - if (!this.TryEnsureCompositeSessionCommandEncoderLocked()) - { - return false; - } + private bool TryDrainQueuedCompositeCommandsLocked() + { + if (!this.compositeSessionGPUActive || this.compositeSessionCommands.Count == 0) + { + return true; + } - if (this.TryRunCompositePassLocked( - this.compositeSessionCommandEncoder, - compositePipeline, - entry, - sourceOffset, - brushData, - blendPercentage, - this.compositeSessionTargetView, - this.compositeSessionTargetWidth, - this.compositeSessionTargetHeight, - destinationX, - destinationY, - sessionCompositeWidth, - sessionCompositeHeight)) - { - this.compositeSessionDirty = true; - return true; - } + if (!this.TryEnsureCompositeSessionCommandEncoderLocked()) + { + return false; + } - this.ResetCompositeSessionStateLocked(); - this.ReleaseCompositeSessionResourcesLocked(); - this.compositeSessionGpuActive = false; + RenderPipeline* compositePipeline = this.GetCompositeSessionPipelineLocked(); + if (compositePipeline is null || this.compositeSessionTargetView is null) + { + return false; + } + + int sessionTargetWidth = this.compositeSessionTargetRectangle.Width; + int sessionTargetHeight = this.compositeSessionTargetRectangle.Height; + + for (int i = 0; i < this.compositeSessionCommands.Count; i++) + { + GPUCompositeCommand command = this.compositeSessionCommands[i]; + if (!this.preparedCoverage.TryGetValue(command.CoverageHandleValue, out CoverageEntry? entry) || + !TryEnsureCoverageTextureLocked(entry)) + { return false; } - return false; + if (!this.TryRunCompositePassLocked( + this.compositeSessionCommandEncoder, + compositePipeline, + entry, + command.SourceOffset, + command.BrushData, + command.BlendPercentage, + this.compositeSessionTargetView, + sessionTargetWidth, + sessionTargetHeight, + command.DestinationX, + command.DestinationY, + command.CompositeWidth, + command.CompositeHeight)) + { + return false; + } } + + this.compositeSessionCommands.Clear(); + return true; } private bool TryEnsureCompositeSessionCommandEncoderLocked() @@ -2861,25 +1740,30 @@ private bool TryEnsureCompositeSessionCommandEncoderLocked() return true; } - if (this.webGpu is null || this.device is null) + if (!this.TryGetGPUState(out GPUState gpuState)) { return false; } CommandEncoderDescriptor commandEncoderDescriptor = default; - this.compositeSessionCommandEncoder = this.webGpu.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + this.compositeSessionCommandEncoder = gpuState.Api.DeviceCreateCommandEncoder(gpuState.Device, in commandEncoderDescriptor); return this.compositeSessionCommandEncoder is not null; } private void TryCloseCompositeSessionPassLocked() { - if (this.compositeSessionPassEncoder is null || this.webGpu is null) + if (this.compositeSessionPassEncoder is null) + { + return; + } + + if (!this.TryGetGPUState(out GPUState gpuState)) { return; } - this.webGpu.RenderPassEncoderEnd(this.compositeSessionPassEncoder); - this.webGpu.RenderPassEncoderRelease(this.compositeSessionPassEncoder); + gpuState.Api.RenderPassEncoderEnd(this.compositeSessionPassEncoder); + gpuState.Api.RenderPassEncoderRelease(this.compositeSessionPassEncoder); this.compositeSessionPassEncoder = null; } @@ -2898,7 +1782,7 @@ private void TryCloseCompositeSessionPassLocked() private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) { - if (entry.GpuCoverageTexture is not null && entry.GpuCoverageView is not null) + if (entry.GPUCoverageTexture is not null && entry.GPUCoverageView is not null) { return true; } @@ -2911,20 +1795,23 @@ private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) WgpuBuffer* uniformBuffer, uint uniformDataSize) { - if (this.webGpu is null || - this.device is null || - this.compositeBindGroupLayout is null || - coverageEntry.GpuCoverageView is null || + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return null; + } + + if (this.compositeBindGroupLayout is null || + coverageEntry.GPUCoverageView is null || uniformBuffer is null || uniformDataSize == 0) { return null; } - if (coverageEntry.GpuCompositeBindGroup is not null && - coverageEntry.GpuCompositeUniformBuffer == uniformBuffer) + if (coverageEntry.GPUCompositeBindGroup is not null && + coverageEntry.GPUCompositeUniformBuffer == uniformBuffer) { - return coverageEntry.GpuCompositeBindGroup; + return coverageEntry.GPUCompositeBindGroup; } this.ReleaseCoverageCompositeBindGroupLocked(coverageEntry); @@ -2933,7 +1820,7 @@ uniformBuffer is null || bindGroupEntries[0] = new BindGroupEntry { Binding = 0, - TextureView = coverageEntry.GpuCoverageView + TextureView = coverageEntry.GPUCoverageView }; bindGroupEntries[1] = new BindGroupEntry { @@ -2950,14 +1837,14 @@ uniformBuffer is null || Entries = bindGroupEntries }; - BindGroup* bindGroup = this.webGpu.DeviceCreateBindGroup(this.device, in bindGroupDescriptor); + BindGroup* bindGroup = gpuState.Api.DeviceCreateBindGroup(gpuState.Device, in bindGroupDescriptor); if (bindGroup is null) { return null; } - coverageEntry.GpuCompositeBindGroup = bindGroup; - coverageEntry.GpuCompositeUniformBuffer = uniformBuffer; + coverageEntry.GPUCompositeBindGroup = bindGroup; + coverageEntry.GPUCompositeUniformBuffer = uniformBuffer; return bindGroup; } @@ -2969,7 +1856,7 @@ private bool TryRunCompositePassLocked( RenderPipeline* compositePipeline, CoverageEntry coverageEntry, Point sourceOffset, - WebGpuBrushData brushData, + WebGPUBrushData brushData, float blendPercentage, TextureView* targetView, int targetWidth, @@ -2979,12 +1866,14 @@ private bool TryRunCompositePassLocked( int compositeWidth, int compositeHeight) { - if (this.webGpu is null || - this.device is null || - this.queue is null || - compositePipeline is null || + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return false; + } + + if (compositePipeline is null || this.compositeBindGroupLayout is null || - coverageEntry.GpuCoverageView is null || + coverageEntry.GPUCoverageView is null || targetView is null || targetWidth <= 0 || targetHeight <= 0) @@ -3040,8 +1929,8 @@ targetView is null || BlendPercentage = blendPercentage }; - this.webGpu.QueueWriteBuffer( - this.queue, + gpuState.Api.QueueWriteBuffer( + gpuState.Queue, this.compositeSessionUniformBuffer, uniformOffset, ref parameters, @@ -3064,7 +1953,7 @@ targetView is null || ColorAttachments = &colorAttachment }; - this.compositeSessionPassEncoder = this.webGpu.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + this.compositeSessionPassEncoder = gpuState.Api.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); if (this.compositeSessionPassEncoder is null) { return false; @@ -3074,9 +1963,9 @@ targetView is null || uint dynamicOffset = uniformOffset; uint* dynamicOffsets = &dynamicOffset; - this.webGpu.RenderPassEncoderSetPipeline(this.compositeSessionPassEncoder, compositePipeline); - this.webGpu.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 1, dynamicOffsets); - this.webGpu.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, 1, 0, 0); + gpuState.Api.RenderPassEncoderSetPipeline(this.compositeSessionPassEncoder, compositePipeline); + gpuState.Api.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 1, dynamicOffsets); + gpuState.Api.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, 1, 0, 0); return true; } @@ -3084,7 +1973,12 @@ private bool TryMapReadBufferLocked(WgpuBuffer* readbackBuffer, nuint byteCount, { mappedData = null; - if (this.webGpu is null || readbackBuffer is null) + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return false; + } + + if (readbackBuffer is null) { return false; } @@ -3100,7 +1994,7 @@ void Callback(BufferMapAsyncStatus status, void* userDataPtr) } using PfnBufferMapCallback callbackPtr = PfnBufferMapCallback.From(Callback); - this.webGpu.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); + gpuState.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); if (!this.WaitForSignalLocked(callbackReady) || mapStatus != BufferMapAsyncStatus.Success) { @@ -3109,10 +2003,10 @@ void Callback(BufferMapAsyncStatus status, void* userDataPtr) } Trace("TryReadBackBufferLocked: map callback success"); - void* rawMappedData = this.webGpu.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); + void* rawMappedData = gpuState.Api.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); if (rawMappedData is null) { - this.webGpu.BufferUnmap(readbackBuffer); + gpuState.Api.BufferUnmap(readbackBuffer); Trace("TryReadBackBufferLocked: mapped range null"); return false; } @@ -3170,7 +2064,10 @@ private bool TryReadBackBufferToRegionLocked( } finally { - this.webGpu?.BufferUnmap(readbackBuffer); + if (this.TryGetGPUState(out GPUState gpuState)) + { + gpuState.Api.BufferUnmap(readbackBuffer); + } Trace("TryReadBackBufferLocked: completed"); } @@ -3179,22 +2076,22 @@ private bool TryReadBackBufferToRegionLocked( private void ReleaseCoverageTextureLocked(CoverageEntry entry) { this.ReleaseCoverageCompositeBindGroupLocked(entry); - Trace($"ReleaseCoverageTextureLocked: tex={(nint)entry.GpuCoverageTexture:X} view={(nint)entry.GpuCoverageView:X}"); - this.ReleaseTextureViewLocked(entry.GpuCoverageView); - this.ReleaseTextureLocked(entry.GpuCoverageTexture); - entry.GpuCoverageView = null; - entry.GpuCoverageTexture = null; + Trace($"ReleaseCoverageTextureLocked: tex={(nint)entry.GPUCoverageTexture:X} view={(nint)entry.GPUCoverageView:X}"); + this.ReleaseTextureViewLocked(entry.GPUCoverageView); + this.ReleaseTextureLocked(entry.GPUCoverageTexture); + entry.GPUCoverageView = null; + entry.GPUCoverageTexture = null; } private void ReleaseCoverageCompositeBindGroupLocked(CoverageEntry entry) { - if (entry.GpuCompositeBindGroup is not null && this.webGpu is not null) + if (entry.GPUCompositeBindGroup is not null && this.TryGetGPUState(out GPUState gpuState)) { - this.webGpu.BindGroupRelease(entry.GpuCompositeBindGroup); + gpuState.Api.BindGroupRelease(entry.GPUCompositeBindGroup); } - entry.GpuCompositeBindGroup = null; - entry.GpuCompositeUniformBuffer = null; + entry.GPUCompositeBindGroup = null; + entry.GPUCompositeUniformBuffer = null; } private void ReleaseAllCoverageCompositeBindGroupsLocked() @@ -3205,23 +2102,6 @@ private void ReleaseAllCoverageCompositeBindGroupsLocked() } } - private void ReleaseCoverageScratchResourcesLocked() - { - this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); - this.ReleaseTextureViewLocked(this.coverageScratchStencilView); - this.ReleaseTextureLocked(this.coverageScratchStencilTexture); - this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); - this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); - this.coverageScratchVertexBuffer = null; - this.coverageScratchVertexCapacityBytes = 0; - this.coverageScratchStencilView = null; - this.coverageScratchStencilTexture = null; - this.coverageScratchMultisampleView = null; - this.coverageScratchMultisampleTexture = null; - this.coverageScratchWidth = 0; - this.coverageScratchHeight = 0; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; @@ -3244,44 +2124,57 @@ private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memo return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetGPUState(out GPUState state) + { + if (this.webGPU is null || this.device is null || this.queue is null) + { + state = default; + return false; + } + + state = new GPUState(this.webGPU, this.device, this.queue); + return true; + } + private void ReleaseTextureViewLocked(TextureView* textureView) { - if (textureView is null || this.webGpu is null) + if (textureView is null || !this.TryGetGPUState(out GPUState gpuState)) { return; } - this.webGpu.TextureViewRelease(textureView); + gpuState.Api.TextureViewRelease(textureView); } private void ReleaseTextureLocked(Texture* texture) { - if (texture is null || this.webGpu is null) + if (texture is null || !this.TryGetGPUState(out GPUState gpuState)) { return; } - this.webGpu.TextureRelease(texture); + gpuState.Api.TextureRelease(texture); } private void ReleaseBufferLocked(WgpuBuffer* buffer) { - if (buffer is null || this.webGpu is null) + if (buffer is null || !this.TryGetGPUState(out GPUState gpuState)) { return; } - this.webGpu.BufferRelease(buffer); + gpuState.Api.BufferRelease(buffer); } private void TryDestroyAndDrainDeviceLocked() { - if (this.webGpu is null || this.device is null) + if (this.webGPU is null || this.device is null) { return; } - this.webGpu.DeviceDestroy(this.device); + this.webGPU.DeviceDestroy(this.device); if (this.wgpuExtension is not null) { @@ -3293,55 +2186,27 @@ private void TryDestroyAndDrainDeviceLocked() if (this.instance is not null) { - this.webGpu.InstanceProcessEvents(this.instance); - this.webGpu.InstanceProcessEvents(this.instance); + this.webGPU.InstanceProcessEvents(this.instance); + this.webGPU.InstanceProcessEvents(this.instance); } } - private void ReleaseGpuResourcesLocked() + private void ReleaseGPUResourcesLocked() { - Trace("ReleaseGpuResourcesLocked: begin"); + Trace("ReleaseGPUResourcesLocked: begin"); this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); - this.ReleaseCoverageScratchResourcesLocked(); - if (this.webGpu is not null) + if (this.webGPU is not null) { - if (this.coverageCoverPipeline is not null) - { - this.webGpu.RenderPipelineRelease(this.coverageCoverPipeline); - this.coverageCoverPipeline = null; - } - - if (this.coverageStencilNonZeroDecrementPipeline is not null) - { - this.webGpu.RenderPipelineRelease(this.coverageStencilNonZeroDecrementPipeline); - this.coverageStencilNonZeroDecrementPipeline = null; - } - - if (this.coverageStencilNonZeroIncrementPipeline is not null) - { - this.webGpu.RenderPipelineRelease(this.coverageStencilNonZeroIncrementPipeline); - this.coverageStencilNonZeroIncrementPipeline = null; - } - - if (this.coverageStencilEvenOddPipeline is not null) - { - this.webGpu.RenderPipelineRelease(this.coverageStencilEvenOddPipeline); - this.coverageStencilEvenOddPipeline = null; - } - - if (this.coveragePipelineLayout is not null) - { - this.webGpu.PipelineLayoutRelease(this.coveragePipelineLayout); - this.coveragePipelineLayout = null; - } + this.coverageRasterizer?.Release(); + this.coverageRasterizer = null; foreach (KeyValuePair compositePipelineEntry in this.compositePipelines) { if (compositePipelineEntry.Value != 0) { - this.webGpu.RenderPipelineRelease((RenderPipeline*)compositePipelineEntry.Value); + this.webGPU.RenderPipelineRelease((RenderPipeline*)compositePipelineEntry.Value); } } @@ -3349,13 +2214,13 @@ private void ReleaseGpuResourcesLocked() if (this.compositePipelineLayout is not null) { - this.webGpu.PipelineLayoutRelease(this.compositePipelineLayout); + this.webGPU.PipelineLayoutRelease(this.compositePipelineLayout); this.compositePipelineLayout = null; } if (this.compositeBindGroupLayout is not null) { - this.webGpu.BindGroupLayoutRelease(this.compositeBindGroupLayout); + this.webGPU.BindGroupLayoutRelease(this.compositeBindGroupLayout); this.compositeBindGroupLayout = null; } @@ -3366,38 +2231,38 @@ private void ReleaseGpuResourcesLocked() if (this.queue is not null) { - this.webGpu.QueueRelease(this.queue); + this.webGPU.QueueRelease(this.queue); this.queue = null; } if (this.device is not null) { - this.webGpu.DeviceRelease(this.device); + this.webGPU.DeviceRelease(this.device); this.device = null; } if (this.adapter is not null) { - this.webGpu.AdapterRelease(this.adapter); + this.webGPU.AdapterRelease(this.adapter); this.adapter = null; } if (this.instance is not null) { - this.webGpu.InstanceRelease(this.instance); + this.webGPU.InstanceRelease(this.instance); this.instance = null; } - this.webGpu = null; + this.webGPU = null; } this.wgpuExtension = null; this.runtimeLease?.Dispose(); this.runtimeLease = null; - this.IsGpuReady = false; - this.compositeSessionGpuActive = false; + this.IsGPUReady = false; + this.compositeSessionGPUActive = false; this.compositeSessionDepth = 0; - Trace("ReleaseGpuResourcesLocked: end"); + Trace("ReleaseGPUResourcesLocked: end"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -3427,47 +2292,59 @@ private struct CompositeParams } [StructLayout(LayoutKind.Sequential)] - private struct StencilVertex - { - public float X; - public float Y; - } - - private readonly struct CoverageSegment + private readonly struct GPUCompositeCommand { - public CoverageSegment(float fromX, float fromY, float toX, float toY) + public GPUCompositeCommand( + int coverageHandleValue, + Point sourceOffset, + WebGPUBrushData brushData, + float blendPercentage, + int destinationX, + int destinationY, + int compositeWidth, + int compositeHeight) { - this.FromX = fromX; - this.FromY = fromY; - this.ToX = toX; - this.ToY = toY; + this.CoverageHandleValue = coverageHandleValue; + this.SourceOffset = sourceOffset; + this.BrushData = brushData; + this.BlendPercentage = blendPercentage; + this.DestinationX = destinationX; + this.DestinationY = destinationY; + this.CompositeWidth = compositeWidth; + this.CompositeHeight = compositeHeight; } - public float FromX { get; } + public int CoverageHandleValue { get; } + + public Point SourceOffset { get; } + + public WebGPUBrushData BrushData { get; } + + public float BlendPercentage { get; } - public float FromY { get; } + public int DestinationX { get; } - public float ToX { get; } + public int DestinationY { get; } - public float ToY { get; } + public int CompositeWidth { get; } + + public int CompositeHeight { get; } } - private readonly struct CoverageTriangleData + private readonly struct GPUState { - public CoverageTriangleData(StencilVertex[] vertices, uint incrementVertexCount, uint decrementVertexCount) + public GPUState(WebGPU api, Device* device, Queue* queue) { - this.Vertices = vertices; - this.IncrementVertexCount = incrementVertexCount; - this.DecrementVertexCount = decrementVertexCount; + this.Api = api; + this.Device = device; + this.Queue = queue; } - public StencilVertex[] Vertices { get; } - - public uint IncrementVertexCount { get; } + public WebGPU Api { get; } - public uint DecrementVertexCount { get; } + public Device* Device { get; } - public uint TotalVertexCount => this.IncrementVertexCount + this.DecrementVertexCount; + public Queue* Queue { get; } } private sealed class CoverageEntry : IDisposable @@ -3482,17 +2359,13 @@ public CoverageEntry(int width, int height) public int Height { get; } - public DrawingCoverageHandle FallbackCoverageHandle { get; set; } - - public bool IsFallback => this.FallbackCoverageHandle.IsValid; - - public Texture* GpuCoverageTexture { get; set; } + public Texture* GPUCoverageTexture { get; set; } - public TextureView* GpuCoverageView { get; set; } + public TextureView* GPUCoverageView { get; set; } - public BindGroup* GpuCompositeBindGroup { get; set; } + public BindGroup* GPUCompositeBindGroup { get; set; } - public WgpuBuffer* GpuCompositeUniformBuffer { get; set; } + public WgpuBuffer* GPUCompositeUniformBuffer { get; set; } public void Dispose() { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs new file mode 100644 index 000000000..4a9c609d5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs @@ -0,0 +1,1059 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Owns WebGPU coverage rasterization resources and converts vector paths into reusable +/// coverage textures using a stencil-and-cover render pass. +/// +internal sealed unsafe class WebGPURasterizer +{ + private const uint CoverageCoverVertexCount = 3; + private const uint CoverageSampleCount = 4; + + private readonly WebGPU webGPU; + private readonly Device* device; + private readonly Queue* queue; + + private PipelineLayout* coveragePipelineLayout; + private RenderPipeline* coverageStencilEvenOddPipeline; + private RenderPipeline* coverageStencilNonZeroIncrementPipeline; + private RenderPipeline* coverageStencilNonZeroDecrementPipeline; + private RenderPipeline* coverageCoverPipeline; + private Texture* coverageScratchMultisampleTexture; + private TextureView* coverageScratchMultisampleView; + private Texture* coverageScratchStencilTexture; + private TextureView* coverageScratchStencilView; + private int coverageScratchWidth; + private int coverageScratchHeight; + private WgpuBuffer* coverageScratchVertexBuffer; + private ulong coverageScratchVertexCapacityBytes; + + public WebGPURasterizer(WebGPU webGPU, Device* device, Queue* queue) + { + this.webGPU = webGPU; + this.device = device; + this.queue = queue; + } + + private static ReadOnlySpan CoverageStencilVertexEntryPoint => "vs_edge\0"u8; + + private static ReadOnlySpan CoverageStencilFragmentEntryPoint => "fs_stencil\0"u8; + + private static ReadOnlySpan CoverageCoverVertexEntryPoint => "vs_cover\0"u8; + + private static ReadOnlySpan CoverageCoverFragmentEntryPoint => "fs_cover\0"u8; + + public bool IsInitialized => + this.coveragePipelineLayout is not null && + this.coverageStencilEvenOddPipeline is not null && + this.coverageStencilNonZeroIncrementPipeline is not null && + this.coverageStencilNonZeroDecrementPipeline is not null && + this.coverageCoverPipeline is not null; + + public bool Initialize() + { + if (this.IsInitialized) + { + return true; + } + + return this.TryCreateCoveragePipelineLocked(); + } + + public bool TryCreateCoverageTexture( + IPath path, + in RasterizerOptions rasterizerOptions, + out Texture* coverageTexture, + out TextureView* coverageView) + { + coverageTexture = null; + coverageView = null; + + if (!this.IsInitialized) + { + return false; + } + + if (!TryBuildCoverageTriangles( + path, + rasterizerOptions.Interest.Location, + rasterizerOptions.Interest.Size, + rasterizerOptions.SamplingOrigin, + out CoverageTriangleData coverageTriangleData)) + { + return false; + } + + return this.TryRasterizeCoverageTextureLocked(in coverageTriangleData, in rasterizerOptions, out coverageTexture, out coverageView); + } + + public void Release() + { + this.ReleaseCoverageScratchResourcesLocked(); + + if (this.coverageCoverPipeline is not null) + { + this.webGPU.RenderPipelineRelease(this.coverageCoverPipeline); + this.coverageCoverPipeline = null; + } + + if (this.coverageStencilNonZeroDecrementPipeline is not null) + { + this.webGPU.RenderPipelineRelease(this.coverageStencilNonZeroDecrementPipeline); + this.coverageStencilNonZeroDecrementPipeline = null; + } + + if (this.coverageStencilNonZeroIncrementPipeline is not null) + { + this.webGPU.RenderPipelineRelease(this.coverageStencilNonZeroIncrementPipeline); + this.coverageStencilNonZeroIncrementPipeline = null; + } + + if (this.coverageStencilEvenOddPipeline is not null) + { + this.webGPU.RenderPipelineRelease(this.coverageStencilEvenOddPipeline); + this.coverageStencilEvenOddPipeline = null; + } + + if (this.coveragePipelineLayout is not null) + { + this.webGPU.PipelineLayoutRelease(this.coveragePipelineLayout); + this.coveragePipelineLayout = null; + } + } + + /// + /// Creates the render pipeline used for coverage rasterization. + /// + private bool TryCreateCoveragePipelineLocked() + { + PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + BindGroupLayoutCount = 0, + BindGroupLayouts = null + }; + + this.coveragePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); + if (this.coveragePipelineLayout is null) + { + return false; + } + + ShaderModule* shaderModule = null; + try + { + ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct + { + SType = SType.ShaderModuleWgslDescriptor + }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + shaderModule = this.webGPU.DeviceCreateShaderModule(this.device, in shaderDescriptor); + } + + if (shaderModule is null) + { + return false; + } + + ReadOnlySpan stencilVertexEntryPoint = CoverageStencilVertexEntryPoint; + ReadOnlySpan stencilFragmentEntryPoint = CoverageStencilFragmentEntryPoint; + ReadOnlySpan coverVertexEntryPoint = CoverageCoverVertexEntryPoint; + ReadOnlySpan coverFragmentEntryPoint = CoverageCoverFragmentEntryPoint; + fixed (byte* stencilVertexEntryPointPtr = stencilVertexEntryPoint) + { + fixed (byte* stencilFragmentEntryPointPtr = stencilFragmentEntryPoint) + { + VertexAttribute* stencilVertexAttributes = stackalloc VertexAttribute[1]; + stencilVertexAttributes[0] = new VertexAttribute + { + Format = VertexFormat.Float32x2, + Offset = 0, + ShaderLocation = 0 + }; + + VertexBufferLayout* stencilVertexBuffers = stackalloc VertexBufferLayout[1]; + stencilVertexBuffers[0] = new VertexBufferLayout + { + ArrayStride = (ulong)Unsafe.SizeOf(), + StepMode = VertexStepMode.Vertex, + AttributeCount = 1, + Attributes = stencilVertexAttributes + }; + + VertexState stencilVertexState = new() + { + Module = shaderModule, + EntryPoint = stencilVertexEntryPointPtr, + BufferCount = 1, + Buffers = stencilVertexBuffers + }; + + ColorTargetState* stencilColorTargets = stackalloc ColorTargetState[1]; + stencilColorTargets[0] = new ColorTargetState + { + Format = TextureFormat.R8Unorm, + Blend = null, + WriteMask = ColorWriteMask.None + }; + + FragmentState stencilFragmentState = new() + { + Module = shaderModule, + EntryPoint = stencilFragmentEntryPointPtr, + TargetCount = 1, + Targets = stencilColorTargets + }; + + PrimitiveState primitiveState = new() + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }; + + MultisampleState multisampleState = new() + { + Count = CoverageSampleCount, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }; + + StencilFaceState evenOddStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.Invert + }; + + DepthStencilState evenOddDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = evenOddStencilFace, + StencilBack = evenOddStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor evenOddPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = primitiveState, + DepthStencil = &evenOddDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilEvenOddPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in evenOddPipelineDescriptor); + if (this.coverageStencilEvenOddPipeline is null) + { + return false; + } + + StencilFaceState incrementStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.IncrementWrap + }; + + DepthStencilState incrementDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = incrementStencilFace, + StencilBack = incrementStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor incrementPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = primitiveState, + DepthStencil = &incrementDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilNonZeroIncrementPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in incrementPipelineDescriptor); + if (this.coverageStencilNonZeroIncrementPipeline is null) + { + return false; + } + + StencilFaceState decrementStencilFace = new() + { + Compare = CompareFunction.Always, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.DecrementWrap + }; + + DepthStencilState decrementDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = decrementStencilFace, + StencilBack = decrementStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = uint.MaxValue, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor decrementPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = stencilVertexState, + Primitive = primitiveState, + DepthStencil = &decrementDepthStencilState, + Multisample = multisampleState, + Fragment = &stencilFragmentState + }; + + this.coverageStencilNonZeroDecrementPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in decrementPipelineDescriptor); + if (this.coverageStencilNonZeroDecrementPipeline is null) + { + return false; + } + } + } + + fixed (byte* coverVertexEntryPointPtr = coverVertexEntryPoint) + { + fixed (byte* coverFragmentEntryPointPtr = coverFragmentEntryPoint) + { + VertexState coverVertexState = new() + { + Module = shaderModule, + EntryPoint = coverVertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + ColorTargetState* coverColorTargets = stackalloc ColorTargetState[1]; + coverColorTargets[0] = new ColorTargetState + { + Format = TextureFormat.R8Unorm, + Blend = null, + WriteMask = ColorWriteMask.Red + }; + + FragmentState coverFragmentState = new() + { + Module = shaderModule, + EntryPoint = coverFragmentEntryPointPtr, + TargetCount = 1, + Targets = coverColorTargets + }; + + StencilFaceState coverStencilFace = new() + { + Compare = CompareFunction.NotEqual, + FailOp = StencilOperation.Keep, + DepthFailOp = StencilOperation.Keep, + PassOp = StencilOperation.Keep + }; + + DepthStencilState coverDepthStencilState = new() + { + Format = TextureFormat.Depth24PlusStencil8, + DepthWriteEnabled = false, + DepthCompare = CompareFunction.Always, + StencilFront = coverStencilFace, + StencilBack = coverStencilFace, + StencilReadMask = uint.MaxValue, + StencilWriteMask = 0, + DepthBias = 0, + DepthBiasSlopeScale = 0F, + DepthBiasClamp = 0F + }; + + RenderPipelineDescriptor coverPipelineDescriptor = new() + { + Layout = this.coveragePipelineLayout, + Vertex = coverVertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = &coverDepthStencilState, + Multisample = new MultisampleState + { + Count = CoverageSampleCount, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &coverFragmentState + }; + + this.coverageCoverPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in coverPipelineDescriptor); + } + } + + return this.coverageCoverPipeline is not null; + } + finally + { + if (shaderModule is not null) + { + this.webGPU.ShaderModuleRelease(shaderModule); + } + } + } + + private bool TryEnsureCoverageScratchTargetsLocked( + int width, + int height, + out TextureView* multisampleCoverageView, + out TextureView* stencilView) + { + multisampleCoverageView = null; + stencilView = null; + + if (this.coverageScratchMultisampleView is not null && + this.coverageScratchStencilView is not null && + this.coverageScratchWidth == width && + this.coverageScratchHeight == height) + { + multisampleCoverageView = this.coverageScratchMultisampleView; + stencilView = this.coverageScratchStencilView; + return true; + } + + this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); + this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); + this.ReleaseTextureViewLocked(this.coverageScratchStencilView); + this.ReleaseTextureLocked(this.coverageScratchStencilTexture); + this.coverageScratchMultisampleView = null; + this.coverageScratchMultisampleTexture = null; + this.coverageScratchStencilView = null; + this.coverageScratchStencilTexture = null; + this.coverageScratchWidth = 0; + this.coverageScratchHeight = 0; + + TextureDescriptor multisampleCoverageTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = CoverageSampleCount + }; + + Texture* createdMultisampleCoverageTexture = + this.webGPU.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); + if (createdMultisampleCoverageTexture is null) + { + return false; + } + + TextureViewDescriptor coverageViewDescriptor = new() + { + Format = TextureFormat.R8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* createdMultisampleCoverageView = this.webGPU.TextureCreateView(createdMultisampleCoverageTexture, in coverageViewDescriptor); + if (createdMultisampleCoverageView is null) + { + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + TextureDescriptor stencilTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = TextureFormat.Depth24PlusStencil8, + MipLevelCount = 1, + SampleCount = CoverageSampleCount + }; + + Texture* createdStencilTexture = this.webGPU.DeviceCreateTexture(this.device, in stencilTextureDescriptor); + if (createdStencilTexture is null) + { + this.ReleaseTextureViewLocked(createdMultisampleCoverageView); + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + TextureViewDescriptor stencilViewDescriptor = new() + { + Format = TextureFormat.Depth24PlusStencil8, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* createdStencilView = this.webGPU.TextureCreateView(createdStencilTexture, in stencilViewDescriptor); + if (createdStencilView is null) + { + this.ReleaseTextureLocked(createdStencilTexture); + this.ReleaseTextureViewLocked(createdMultisampleCoverageView); + this.ReleaseTextureLocked(createdMultisampleCoverageTexture); + return false; + } + + this.coverageScratchMultisampleTexture = createdMultisampleCoverageTexture; + this.coverageScratchMultisampleView = createdMultisampleCoverageView; + this.coverageScratchStencilTexture = createdStencilTexture; + this.coverageScratchStencilView = createdStencilView; + this.coverageScratchWidth = width; + this.coverageScratchHeight = height; + + multisampleCoverageView = createdMultisampleCoverageView; + stencilView = createdStencilView; + return true; + } + + private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) + { + if (this.coverageScratchVertexBuffer is not null && + this.coverageScratchVertexCapacityBytes >= requiredByteCount) + { + return true; + } + + this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); + this.coverageScratchVertexBuffer = null; + this.coverageScratchVertexCapacityBytes = 0; + + BufferDescriptor vertexBufferDescriptor = new() + { + Usage = BufferUsage.Vertex | BufferUsage.CopyDst, + Size = requiredByteCount + }; + + WgpuBuffer* createdVertexBuffer = this.webGPU.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); + if (createdVertexBuffer is null) + { + return false; + } + + this.coverageScratchVertexBuffer = createdVertexBuffer; + this.coverageScratchVertexCapacityBytes = requiredByteCount; + return true; + } + + /// + /// Rasterizes edge triangles through a stencil-and-cover pass into an R8Unorm texture. + /// + private bool TryRasterizeCoverageTextureLocked( + in CoverageTriangleData coverageTriangleData, + in RasterizerOptions rasterizerOptions, + out Texture* coverageTexture, + out TextureView* coverageView) + { + coverageTexture = null; + coverageView = null; + + Texture* createdCoverageTexture = null; + TextureView* createdCoverageView = null; + CommandEncoder* commandEncoder = null; + RenderPassEncoder* passEncoder = null; + CommandBuffer* commandBuffer = null; + bool success = false; + try + { + if (!this.TryEnsureCoverageScratchTargetsLocked( + rasterizerOptions.Interest.Width, + rasterizerOptions.Interest.Height, + out TextureView* multisampleCoverageView, + out TextureView* stencilView)) + { + return false; + } + + TextureDescriptor coverageTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), + Format = TextureFormat.R8Unorm, + MipLevelCount = 1, + SampleCount = 1 + }; + + createdCoverageTexture = this.webGPU.DeviceCreateTexture(this.device, in coverageTextureDescriptor); + if (createdCoverageTexture is null) + { + return false; + } + + TextureViewDescriptor coverageViewDescriptor = new() + { + Format = TextureFormat.R8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + createdCoverageView = this.webGPU.TextureCreateView(createdCoverageTexture, in coverageViewDescriptor); + if (createdCoverageView is null) + { + return false; + } + + ulong vertexByteCount = checked(coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); + if (!this.TryEnsureCoverageScratchVertexBufferLocked(vertexByteCount) || this.coverageScratchVertexBuffer is null) + { + return false; + } + + fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) + { + this.webGPU.QueueWriteBuffer(this.queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); + } + + CommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.webGPU.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + RenderPassColorAttachment colorAttachment = new() + { + View = multisampleCoverageView, + ResolveTarget = createdCoverageView, + LoadOp = LoadOp.Clear, + StoreOp = StoreOp.Discard, + ClearValue = default + }; + + RenderPassDepthStencilAttachment depthStencilAttachment = new() + { + View = stencilView, + DepthLoadOp = LoadOp.Clear, + DepthStoreOp = StoreOp.Discard, + DepthClearValue = 1F, + DepthReadOnly = false, + StencilLoadOp = LoadOp.Clear, + StencilStoreOp = StoreOp.Discard, + StencilClearValue = 0, + StencilReadOnly = false + }; + + RenderPassDescriptor renderPassDescriptor = new() + { + ColorAttachmentCount = 1, + ColorAttachments = &colorAttachment, + DepthStencilAttachment = &depthStencilAttachment + }; + + passEncoder = this.webGPU.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + if (passEncoder is null) + { + return false; + } + + this.webGPU.RenderPassEncoderSetStencilReference(passEncoder, 0); + this.webGPU.RenderPassEncoderSetVertexBuffer(passEncoder, 0, this.coverageScratchVertexBuffer, 0, vertexByteCount); + if (rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd) + { + this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilEvenOddPipeline); + this.webGPU.RenderPassEncoderDraw(passEncoder, coverageTriangleData.TotalVertexCount, 1, 0, 0); + } + else + { + if (coverageTriangleData.IncrementVertexCount > 0) + { + this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); + this.webGPU.RenderPassEncoderDraw(passEncoder, coverageTriangleData.IncrementVertexCount, 1, 0, 0); + } + + if (coverageTriangleData.DecrementVertexCount > 0) + { + this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); + this.webGPU.RenderPassEncoderDraw( + passEncoder, + coverageTriangleData.DecrementVertexCount, + 1, + coverageTriangleData.IncrementVertexCount, + 0); + } + } + + this.webGPU.RenderPassEncoderSetStencilReference(passEncoder, 0); + this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageCoverPipeline); + this.webGPU.RenderPassEncoderDraw(passEncoder, CoverageCoverVertexCount, 1, 0, 0); + + this.webGPU.RenderPassEncoderEnd(passEncoder); + this.webGPU.RenderPassEncoderRelease(passEncoder); + passEncoder = null; + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.webGPU.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + this.webGPU.QueueSubmit(this.queue, 1, ref commandBuffer); + + this.webGPU.CommandBufferRelease(commandBuffer); + commandBuffer = null; + coverageTexture = createdCoverageTexture; + coverageView = createdCoverageView; + createdCoverageTexture = null; + createdCoverageView = null; + success = true; + return true; + } + finally + { + if (passEncoder is not null) + { + this.webGPU.RenderPassEncoderRelease(passEncoder); + } + + if (commandBuffer is not null) + { + this.webGPU.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.webGPU.CommandEncoderRelease(commandEncoder); + } + + if (!success) + { + this.ReleaseTextureViewLocked(createdCoverageView); + this.ReleaseTextureLocked(createdCoverageTexture); + } + } + } + + /// + /// Flattens a path into local-interest coordinates and converts each non-horizontal edge + /// into a trapezoid (two triangles) anchored at a left-side sentinel X. + /// + private static bool TryBuildCoverageTriangles( + IPath path, + Point interestLocation, + Size interestSize, + RasterizerSamplingOrigin samplingOrigin, + out CoverageTriangleData coverageTriangleData) + { + coverageTriangleData = default; + if (interestSize.Width <= 0 || interestSize.Height <= 0) + { + return false; + } + + float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelBoundary ? 0.5F : 0F; + float offsetX = sampleShift - interestLocation.X; + float offsetY = sampleShift - interestLocation.Y; + + List segments = []; + float minX = float.PositiveInfinity; + + foreach (ISimplePath simplePath in path.Flatten()) + { + ReadOnlySpan points = simplePath.Points.Span; + if (points.Length < 2) + { + continue; + } + + for (int i = 1; i < points.Length; i++) + { + AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX); + } + + if (simplePath.IsClosed) + { + AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX); + } + } + + if (segments.Count == 0 || !float.IsFinite(minX)) + { + return false; + } + + int incrementEdgeCount = 0; + int decrementEdgeCount = 0; + foreach (CoverageSegment segment in segments) + { + if (segment.FromY == segment.ToY) + { + continue; + } + + if (segment.ToY > segment.FromY) + { + incrementEdgeCount++; + } + else + { + decrementEdgeCount++; + } + } + + int totalEdgeCount = incrementEdgeCount + decrementEdgeCount; + if (totalEdgeCount == 0) + { + return false; + } + + float sentinelX = minX - 1F; + float widthScale = 2F / interestSize.Width; + float heightScale = 2F / interestSize.Height; + int incrementVertexCount = checked(incrementEdgeCount * 6); + int decrementVertexCount = checked(decrementEdgeCount * 6); + StencilVertex[] vertices = new StencilVertex[checked(incrementVertexCount + decrementVertexCount)]; + + int vertexIndex = 0; + foreach (CoverageSegment segment in segments) + { + if (segment.ToY <= segment.FromY) + { + continue; + } + + AppendCoverageEdgeQuad( + vertices, + ref vertexIndex, + sentinelX, + segment.FromX, + segment.FromY, + segment.ToX, + segment.ToY, + widthScale, + heightScale); + } + + int decrementStartIndex = incrementVertexCount; + vertexIndex = decrementStartIndex; + foreach (CoverageSegment segment in segments) + { + if (segment.ToY >= segment.FromY) + { + continue; + } + + AppendCoverageEdgeQuad( + vertices, + ref vertexIndex, + sentinelX, + segment.FromX, + segment.FromY, + segment.ToX, + segment.ToY, + widthScale, + heightScale); + } + + coverageTriangleData = new CoverageTriangleData( + vertices, + (uint)incrementVertexCount, + (uint)decrementVertexCount); + return true; + } + + private static void AddCoverageSegment( + PointF from, + PointF to, + float offsetX, + float offsetY, + List destination, + ref float minX) + { + if (from.Equals(to)) + { + return; + } + + if (!float.IsFinite(from.X) || + !float.IsFinite(from.Y) || + !float.IsFinite(to.X) || + !float.IsFinite(to.Y)) + { + return; + } + + float fromX = from.X + offsetX; + float fromY = from.Y + offsetY; + float toX = to.X + offsetX; + float toY = to.Y + offsetY; + + destination.Add(new CoverageSegment(fromX, fromY, toX, toY)); + minX = MathF.Min(minX, MathF.Min(fromX, toX)); + } + + private static void AppendCoverageEdgeQuad( + StencilVertex[] destination, + ref int destinationIndex, + float sentinelX, + float fromX, + float fromY, + float toX, + float toY, + float widthScale, + float heightScale) + { + StencilVertex a = ToStencilVertex(sentinelX, fromY, widthScale, heightScale); + StencilVertex b = ToStencilVertex(fromX, fromY, widthScale, heightScale); + StencilVertex c = ToStencilVertex(toX, toY, widthScale, heightScale); + StencilVertex d = ToStencilVertex(sentinelX, toY, widthScale, heightScale); + + destination[destinationIndex++] = a; + destination[destinationIndex++] = b; + destination[destinationIndex++] = c; + destination[destinationIndex++] = a; + destination[destinationIndex++] = c; + destination[destinationIndex++] = d; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static StencilVertex ToStencilVertex(float x, float y, float widthScale, float heightScale) + => new() + { + X = (x * widthScale) - 1F, + Y = 1F - (y * heightScale) + }; + + private void ReleaseCoverageScratchResourcesLocked() + { + this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); + this.ReleaseTextureViewLocked(this.coverageScratchStencilView); + this.ReleaseTextureLocked(this.coverageScratchStencilTexture); + this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); + this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); + this.coverageScratchVertexBuffer = null; + this.coverageScratchVertexCapacityBytes = 0; + this.coverageScratchStencilView = null; + this.coverageScratchStencilTexture = null; + this.coverageScratchMultisampleView = null; + this.coverageScratchMultisampleTexture = null; + this.coverageScratchWidth = 0; + this.coverageScratchHeight = 0; + } + + private void ReleaseTextureViewLocked(TextureView* textureView) + { + if (textureView is null) + { + return; + } + + this.webGPU.TextureViewRelease(textureView); + } + + private void ReleaseTextureLocked(Texture* texture) + { + if (texture is null) + { + return; + } + + this.webGPU.TextureRelease(texture); + } + + private void ReleaseBufferLocked(WgpuBuffer* buffer) + { + if (buffer is null) + { + return; + } + + this.webGPU.BufferRelease(buffer); + } + + private struct StencilVertex + { + public float X; + public float Y; + } + + private readonly struct CoverageSegment + { + public CoverageSegment(float fromX, float fromY, float toX, float toY) + { + this.FromX = fromX; + this.FromY = fromY; + this.ToX = toX; + this.ToY = toY; + } + + public float FromX { get; } + + public float FromY { get; } + + public float ToX { get; } + + public float ToY { get; } + } + + private readonly struct CoverageTriangleData + { + public CoverageTriangleData(StencilVertex[] vertices, uint incrementVertexCount, uint decrementVertexCount) + { + this.Vertices = vertices; + this.IncrementVertexCount = incrementVertexCount; + this.DecrementVertexCount = decrementVertexCount; + } + + public StencilVertex[] Vertices { get; } + + public uint IncrementVertexCount { get; } + + public uint DecrementVertexCount { get; } + + public uint TotalVertexCount => this.IncrementVertexCount + this.DecrementVertexCount; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs similarity index 98% rename from src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs rename to src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index 1d1efce09..7a4ed931c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGpuRuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// The shutdown path is resilient to duplicate native unload attempts. /// /// -internal static unsafe class WebGpuRuntime +internal static unsafe class WebGPURuntime { /// /// Synchronizes all runtime state transitions. @@ -182,7 +182,7 @@ private static void DisposeRuntimeCore() } /// - /// Ref-counted access token for . + /// Ref-counted access token for . /// /// /// Disposing the lease decrements the runtime lease count exactly once. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs similarity index 83% rename from src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs rename to src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs index 788ea67ca..fe821cbd7 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGpuSurfaceCapability.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -8,22 +8,24 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Native WebGPU surface capability attached to . /// -public sealed class WebGpuSurfaceCapability +public sealed class WebGPUSurfaceCapability { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Opaque WGPUDevice* handle. /// Opaque WGPUQueue* handle. + /// Opaque WGPUTexture* handle for the current frame when writable upload is supported. /// Opaque WGPUTextureView* handle for the current frame. /// Native render target texture format. /// Surface width in pixels. /// Surface height in pixels. /// Whether the target format is sRGB encoded. /// Whether alpha is premultiplied in the target surface. - public WebGpuSurfaceCapability( + public WebGPUSurfaceCapability( nint device, nint queue, + nint targetTexture, nint targetTextureView, TextureFormat targetFormat, int width, @@ -33,6 +35,7 @@ public WebGpuSurfaceCapability( { this.Device = device; this.Queue = queue; + this.TargetTexture = targetTexture; this.TargetTextureView = targetTextureView; this.TargetFormat = targetFormat; this.Width = width; @@ -51,6 +54,11 @@ public WebGpuSurfaceCapability( /// public nint Queue { get; } + /// + /// Gets the opaque WGPUTexture* handle for the current frame. + /// + public nint TargetTexture { get; } + /// /// Gets the opaque WGPUTextureView* handle for the current frame. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs new file mode 100644 index 000000000..26887b181 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -0,0 +1,134 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One normalized composition command queued by . +/// +internal readonly struct CompositionCommand +{ + /// + /// Initializes a new instance of the struct. + /// + /// Stable definition key used for composition-level caching. + /// Path to rasterize in target-local coordinates. + /// Brush used during composition. + /// Brush bounds used for applicator creation. + /// Graphics options used for composition. + /// Rasterizer options used to generate coverage. + private CompositionCommand( + int definitionKey, + IPath path, + Brush brush, + Rectangle brushBounds, + GraphicsOptions graphicsOptions, + RasterizerOptions rasterizerOptions) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); + + this.DefinitionKey = definitionKey; + this.Path = path; + this.Brush = brush; + this.BrushBounds = brushBounds; + this.GraphicsOptions = graphicsOptions; + this.RasterizerOptions = rasterizerOptions; + } + + /// + /// Gets a stable definition key used for composition-level caching. + /// + public int DefinitionKey { get; } + + /// + /// Gets the path to rasterize in target-local coordinates. + /// + public IPath Path { get; } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets graphics options used for composition. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Creates a composition command and computes a stable definition key from path/brush/rasterizer options. + /// + /// Path to rasterize in target-local coordinates. + /// Brush used during composition. + /// Graphics options used for composition. + /// Rasterizer options used to generate coverage. + /// The normalized composition command. + public static CompositionCommand Create( + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + { + HashCode hash = default; + hash.Add(RuntimeHelpers.GetHashCode(path)); + hash.Add(RuntimeHelpers.GetHashCode(brush)); + hash.Add(rasterizerOptions.Interest); + hash.Add((int)rasterizerOptions.IntersectionRule); + hash.Add((int)rasterizerOptions.RasterizationMode); + hash.Add((int)rasterizerOptions.SamplingOrigin); + + return Create( + hash.ToHashCode(), + path, + brush, + graphicsOptions, + rasterizerOptions); + } + + /// + /// Creates a composition command using a caller-provided definition key. + /// + /// Stable definition key used for composition-level caching. + /// Path to rasterize in target-local coordinates. + /// Brush used during composition. + /// Graphics options used for composition. + /// Rasterizer options used to generate coverage. + /// The normalized composition command. + public static CompositionCommand Create( + int definitionKey, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions) + { + RectangleF bounds = path.Bounds; + Rectangle brushBounds = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + return new( + definitionKey, + path, + brush, + brushBounds, + graphicsOptions, + rasterizerOptions); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 3456b9562..811f85cbd 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using System.Collections.Concurrent; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -11,15 +9,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Default drawing backend. /// -/// -/// This backend keeps scanline handling internal so higher-level processors -/// can remain backend-agnostic. -/// internal sealed class DefaultDrawingBackend : IDrawingBackend { - private readonly ConcurrentDictionary> preparedCoverage = new(); - private int nextCoverageHandleId; - /// /// Initializes a new instance of the class. /// @@ -51,22 +42,6 @@ public static DefaultDrawingBackend Create(IRasterizer rasterizer) return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new DefaultDrawingBackend(rasterizer); } - /// - public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - } - - /// - public void EndCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - } - /// public void FillPath( Configuration configuration, @@ -74,548 +49,66 @@ public void FillPath( IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - - if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) - { - throw new NotSupportedException( - $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.FillPath)}."); - } - - FillPath( - configuration, - destinationRegion, - path, - brush, - graphicsOptions, - rasterizerOptions, - configuration.MemoryAllocator, - this.PrimaryRasterizer); - } - - /// - public void FillRegion( - Configuration configuration, - ICanvasFrame target, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle region) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - - if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) - { - throw new NotSupportedException( - $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.FillRegion)}."); - } - - FillRegionCore(configuration, destinationRegion, brush, graphicsOptions, region); - } - - /// - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(brush, nameof(brush)); - _ = graphicsOptions; - return true; - } - - /// - public DrawingCoverageHandle PrepareCoverage( - IPath path, in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - CoveragePreparationMode preparationMode) - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); - _ = preparationMode; - - Size size = rasterizerOptions.Interest.Size; - if (size.Width <= 0 || size.Height <= 0) - { - return default; - } - - Buffer2D destination = allocator.Allocate2D(size, AllocationOptions.Clean); - - CoverageRasterizationState state = new(destination, rasterizerOptions.Interest.Top); - this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessCoverageScanline); - - int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); - if (!this.preparedCoverage.TryAdd(handleId, destination)) - { - destination.Dispose(); - throw new InvalidOperationException("Failed to cache prepared coverage."); - } - - return new DrawingCoverageHandle(handleId); - } - - /// - public void CompositeCoverage( - Configuration configuration, - ICanvasFrame target, - DrawingCoverageHandle coverageHandle, - Point sourceOffset, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) + DrawingCanvasBatcher batcher) where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - Guard.NotNull(brush, nameof(brush)); - - if (!coverageHandle.IsValid) - { - return; - } - - if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) - { - throw new NotSupportedException( - $"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.CompositeCoverage)}."); - } - - if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out Buffer2D? coverageMap)) - { - throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); - } - - if (!CoverageCompositor.TryGetCompositeRegions( - destinationFrame, - coverageMap, - sourceOffset, - out Buffer2DRegion destinationRegion, - out Buffer2DRegion sourceRegion)) - { - return; - } - - CoverageCompositor.CompositeFloatCoverage( - configuration, - destinationRegion, - sourceRegion, - brush, - graphicsOptions, - brushBounds); - } + => batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); /// - public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) - { - if (!coverageHandle.IsValid) - { - return; - } - - if (this.preparedCoverage.TryRemove(coverageHandle.Value, out Buffer2D? coverage)) - { - coverage.Dispose(); - } - } - - /// - /// Fills a path into a destination buffer using the configured rasterizer. - /// - /// The pixel format. - /// Active processing configuration. - /// Destination pixel region. - /// The path to rasterize. - /// Brush used to shade covered pixels. - /// Graphics blending/composition options. - /// Rasterizer options. - /// Allocator for temporary data. - /// Rasterizer implementation. - private static void FillPath( + public void FlushCompositions( Configuration configuration, - Buffer2DRegion destinationRegion, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - IRasterizer rasterizer) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(destinationRegion.Buffer, nameof(destinationRegion)); - Guard.NotNull(path, nameof(path)); - Guard.NotNull(brush, nameof(brush)); - Guard.NotNull(allocator, nameof(allocator)); - Guard.NotNull(rasterizer, nameof(rasterizer)); - - Rectangle destinationLocalBounds = new(0, 0, destinationRegion.Width, destinationRegion.Height); - Rectangle interest = Rectangle.Intersect(rasterizerOptions.Interest, destinationLocalBounds); - if (interest.Equals(Rectangle.Empty)) - { - return; - } - - RasterizerOptions clippedRasterizerOptions = rasterizerOptions; - if (!interest.Equals(rasterizerOptions.Interest)) - { - clippedRasterizerOptions = new RasterizerOptions( - interest, - rasterizerOptions.IntersectionRule, - rasterizerOptions.RasterizationMode, - rasterizerOptions.SamplingOrigin); - } - - // Detect the common "opaque solid without blending" case and bypass brush sampling - // for fully covered runs. - TPixel solidBrushColor = default; - bool isSolidBrushWithoutBlending = false; - if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) - { - isSolidBrushWithoutBlending = true; - solidBrushColor = solidBrush.Color.ToPixel(); - } - - int minX = interest.Left; - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - graphicsOptions, - destinationRegion, - path.Bounds); - - FillRasterizationState state = new( - destinationRegion, - applicator, - minX, - destinationRegion.Rectangle.X, - destinationRegion.Rectangle.Y, - isSolidBrushWithoutBlending, - solidBrushColor); - - rasterizer.Rasterize(path, clippedRasterizerOptions, allocator, ref state, ProcessRasterizedScanline); - } - - /// - /// Fills a region in destination-local coordinates with the provided brush. - /// - /// The pixel format. - /// Active processing configuration. - /// Destination pixel region. - /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - /// Region to fill in destination-local coordinates. - private static void FillRegionCore( - Configuration configuration, - Buffer2DRegion destinationRegion, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle localRegion) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(destinationRegion.Buffer, nameof(destinationRegion)); - Guard.NotNull(brush, nameof(brush)); - - Rectangle destinationLocalBounds = new(0, 0, destinationRegion.Width, destinationRegion.Height); - Rectangle clippedRegion = Rectangle.Intersect(destinationLocalBounds, localRegion); - if (clippedRegion.Equals(Rectangle.Empty)) - { - return; - } - - Buffer2DRegion scopedDestination = destinationRegion.GetSubRegion(clippedRegion); - - if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) - { - TPixel solidBrushColor = solidBrush.Color.ToPixel(); - for (int y = 0; y < scopedDestination.Height; y++) - { - scopedDestination.DangerousGetRowSpan(y).Fill(solidBrushColor); - } - - return; - } - - RectangleF brushRegion = new(clippedRegion.X, clippedRegion.Y, clippedRegion.Width, clippedRegion.Height); - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - graphicsOptions, - scopedDestination, - brushRegion); - using IMemoryOwner amount = configuration.MemoryAllocator.Allocate(scopedDestination.Width); - Span amountSpan = amount.Memory.Span; - amountSpan.Fill(1F); - - int minX = scopedDestination.Rectangle.X; - int minY = scopedDestination.Rectangle.Y; - for (int localY = 0; localY < scopedDestination.Height; localY++) - { - applicator.Apply(amountSpan, minX, minY + localY); - } - } - - /// - /// Dispatches rasterized coverage to either the generic brush path or the opaque-solid fast path. - /// - /// The pixel format. - /// Destination row index. - /// Rasterized coverage row. - /// Callback state. - private static void ProcessRasterizedScanline(int y, Span scanline, ref FillRasterizationState state) + ICanvasFrame target, + IReadOnlyList compositions) where TPixel : unmanaged, IPixel { - int absoluteY = y + state.DestinationOffsetY; - int absoluteMinX = state.MinX + state.DestinationOffsetX; - if (state.IsSolidBrushWithoutBlending) - { - ApplyCoverageRunsForOpaqueSolidBrush( - state.DestinationRegion, - state.Applicator, - scanline, - absoluteMinX, - absoluteY, - state.SolidBrushColor); - } - else - { - ApplyPositiveCoverageRuns(state.Applicator, scanline, absoluteMinX, absoluteY); - } - } + _ = target.TryGetCpuRegion(out Buffer2DRegion destinationFrame); - /// - /// Copies one rasterized coverage row into the destination coverage buffer. - /// - /// Destination row index. - /// Source coverage row. - /// Callback state containing destination storage. - private static void ProcessCoverageScanline(int y, Span scanline, ref CoverageRasterizationState state) - { - int row = y - state.DestinationTop; - Span destination = state.Buffer.DangerousGetRowSpan(row); - scanline.CopyTo(destination); - } + CompositionCommand coverageDefinition = compositions[0]; + using Buffer2D coverageMap = this.CreateCoverageMap(coverageDefinition, configuration.MemoryAllocator); + Buffer2DRegion destinationRegion = destinationFrame.GetSubRegion(coverageDefinition.RasterizerOptions.Interest); - /// - /// Applies a brush to contiguous positive-coverage runs on a scanline. - /// - /// - /// The rasterizer has already resolved the fill rule (NonZero or EvenOdd) into per-pixel - /// coverage values. This method simply consumes the resulting positive runs. - /// - /// The pixel format. - /// Brush applicator. - /// Coverage values for one row. - /// Absolute X of scanline index 0. - /// Destination row index. - private static void ApplyPositiveCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) - where TPixel : unmanaged, IPixel - { - int i = 0; - while (i < scanline.Length) + for (int row = 0; row < coverageMap.Height; row++) { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } + Span rowCoverage = coverageMap.DangerousGetRowSpan(row); + int y = destinationRegion.Rectangle.Y + row; - int runLength = i - runStart; - if (runLength > 0) + for (int i = 0; i < compositions.Count; i++) { - // Apply only the positive-coverage run. This avoids invoking brush logic - // for fully transparent gaps. - applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); + CompositionCommand command = compositions[i]; + + // TODO: This should be optimized to avoid creating multiple applicators + // for the same brush/graphics options. + // We should create them first outside of the loop then dispose after. + using BrushApplicator applicator = command.Brush.CreateApplicator( + configuration, + command.GraphicsOptions, + destinationRegion, + command.BrushBounds); + + applicator.Apply(rowCoverage, destinationRegion.Rectangle.X, y); } } } - /// - /// Applies coverage using a mixed strategy for opaque solid brushes. - /// - /// - /// Semi-transparent edges still go through brush blending, but fully covered interior runs - /// are written directly with . - /// - /// The pixel format. - /// Destination pixel region. - /// Brush applicator for non-opaque segments. - /// Coverage values for one row. - /// Absolute X of scanline index 0. - /// Destination row index. - /// Pre-converted solid color for direct writes. - private static void ApplyCoverageRunsForOpaqueSolidBrush( - Buffer2DRegion destinationRegion, - BrushApplicator applicator, - Span scanline, - int minX, - int y, - TPixel solidBrushColor) - where TPixel : unmanaged, IPixel + private Buffer2D CreateCoverageMap( + CompositionCommand command, + MemoryAllocator allocator) { - int localY = y - destinationRegion.Rectangle.Y; - int localX = minX - destinationRegion.Rectangle.X; - Span destinationRow = destinationRegion.DangerousGetRowSpan(localY).Slice(localX, scanline.Length); - int i = 0; - - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runEnd = i; - if (runEnd <= runStart) - { - continue; - } - - // Leading partially-covered segment. - int opaqueStart = runStart; - while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) - { - opaqueStart++; - } - - if (opaqueStart > runStart) - { - int prefixLength = opaqueStart - runStart; - applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); - } - - // Trailing partially-covered segment. - int opaqueEnd = runEnd; - while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) - { - opaqueEnd--; - } - - // Fully covered interior can skip blending entirely. - if (opaqueEnd > opaqueStart) - { - destinationRow[opaqueStart..opaqueEnd].Fill(solidBrushColor); - } + Size size = command.RasterizerOptions.Interest.Size; + Buffer2D coverage = allocator.Allocate2D(size, AllocationOptions.Clean); - if (runEnd > opaqueEnd) + (Buffer2D Buffer, int DestinationTop) state = (coverage, command.RasterizerOptions.Interest.Top); + this.PrimaryRasterizer.Rasterize( + command.Path, + command.RasterizerOptions, + allocator, + ref state, + static (int y, Span scanline, ref (Buffer2D Buffer, int DestinationTop) callbackState) => { - int suffixLength = runEnd - opaqueEnd; - applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); - } - } - } - - /// - /// Callback state used while writing coverage maps. - /// - private readonly struct CoverageRasterizationState - { - /// - /// Initializes a new instance of the struct. - /// - /// Destination coverage buffer. - /// Absolute Y corresponding to destination row 0. - public CoverageRasterizationState(Buffer2D buffer, int destinationTop) - { - this.Buffer = buffer; - this.DestinationTop = destinationTop; - } - - /// - /// Gets the destination coverage buffer. - /// - public Buffer2D Buffer { get; } - - /// - /// Gets the absolute Y corresponding to destination row 0. - /// - public int DestinationTop { get; } - } - - /// - /// Callback state used while filling into an image frame. - /// - /// The pixel format. - private readonly struct FillRasterizationState - where TPixel : unmanaged, IPixel - { - /// - /// Initializes a new instance of the struct. - /// - /// Destination pixel region. - /// Brush applicator for blended segments. - /// Local X corresponding to scanline index 0. - /// Destination region X offset in target coordinates. - /// Destination region Y offset in target coordinates. - /// - /// Indicates whether opaque solid fast-path writes are allowed. - /// - /// Pre-converted opaque solid color. - public FillRasterizationState( - Buffer2DRegion destinationRegion, - BrushApplicator applicator, - int minX, - int destinationOffsetX, - int destinationOffsetY, - bool isSolidBrushWithoutBlending, - TPixel solidBrushColor) - { - this.DestinationRegion = destinationRegion; - this.Applicator = applicator; - this.MinX = minX; - this.DestinationOffsetX = destinationOffsetX; - this.DestinationOffsetY = destinationOffsetY; - this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; - this.SolidBrushColor = solidBrushColor; - } - - /// - /// Gets the destination pixel region. - /// - public Buffer2DRegion DestinationRegion { get; } - - /// - /// Gets the brush applicator used for blended segments. - /// - public BrushApplicator Applicator { get; } - - /// - /// Gets the local X origin of the current scanline. - /// - public int MinX { get; } - - /// - /// Gets the destination region X offset in target coordinates. - /// - public int DestinationOffsetX { get; } - - /// - /// Gets the destination region Y offset in target coordinates. - /// - public int DestinationOffsetY { get; } - - /// - /// Gets a value indicating whether opaque interior runs can be direct-filled. - /// - public bool IsSolidBrushWithoutBlending { get; } + int row = y - callbackState.DestinationTop; + scanline.CopyTo(callbackState.Buffer.DangerousGetRowSpan(row)); + }); - /// - /// Gets the pre-converted solid color used by the opaque fast path. - /// - public TPixel SolidBrushColor { get; } + return coverage; } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 53f3b34e8..4c06e72ef 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -15,28 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal interface IDrawingBackend { - /// - /// Begins a composition session over a target region. - /// - /// - /// Backends can use this as an optional batching boundary (for example: keep the destination - /// resident on an accelerator while multiple composite calls are applied). - /// - /// The pixel format. - /// Active processing configuration. - /// Destination frame. - public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel; - - /// - /// Ends a composition session over a target region. - /// - /// The pixel format. - /// Active processing configuration. - /// Destination frame. - public void EndCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel; - /// /// Fills a path into a destination target region. /// @@ -47,81 +24,27 @@ public void EndCompositeSession(Configuration configuration, ICanvasFram /// Brush used to shade covered pixels. /// Graphics blending/composition options. /// Rasterizer options in target-local coordinates. + /// Batcher used to queue normalized composition commands. public void FillPath( Configuration configuration, ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) - where TPixel : unmanaged, IPixel; - - /// - /// Fills a local region in a destination target. - /// - /// The pixel format. - /// Active processing configuration. - /// Destination frame. - /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - /// Region in target-local coordinates. - public void FillRegion( - Configuration configuration, - ICanvasFrame target, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle region) - where TPixel : unmanaged, IPixel; - - /// - /// Determines whether this backend can composite coverage using the accelerated path - /// for the given brush/options combination. - /// - /// The pixel format. - /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - /// when accelerated composition is supported. - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) - where TPixel : unmanaged, IPixel; - - /// - /// Prepares coverage for a path and returns a backend-owned handle. - /// - /// The local path to rasterize. - /// Rasterizer options. - /// Allocator for temporary data. - /// Coverage preparation mode ( or ). - /// An opaque handle to prepared coverage data. - public DrawingCoverageHandle PrepareCoverage( - IPath path, in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - CoveragePreparationMode preparationMode); + DrawingCanvasBatcher batcher) + where TPixel : unmanaged, IPixel; /// - /// Composites prepared coverage into a destination region using a brush. + /// Flushes queued composition operations for the target. /// /// The pixel format. /// Active processing configuration. /// Destination frame. - /// Handle to prepared coverage data. - /// Source offset inside the prepared coverage. - /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - /// Brush bounds used when creating the applicator. - public void CompositeCoverage( + /// Queued composition commands in batch order. + public void FlushCompositions( Configuration configuration, ICanvasFrame target, - DrawingCoverageHandle coverageHandle, - Point sourceOffset, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) + IReadOnlyList compositions) where TPixel : unmanaged, IPixel; - - /// - /// Releases a prepared coverage handle. - /// - /// Handle to release. - public void ReleaseCoverage(DrawingCoverageHandle coverageHandle); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs new file mode 100644 index 000000000..d1818cdf9 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +internal sealed class DrawingCanvasBatcher + where TPixel : unmanaged, IPixel +{ + internal DrawingCanvasBatcher( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(backend, nameof(backend)); + Guard.NotNull(targetFrame, nameof(targetFrame)); + } + + public void AddComposition(in CompositionCommand composition) + { + _ = composition; + // Stub: implementation is added after backend contracts are wired. + } + + public void FlushCompositions() + { + // Stub: implementation is added after backend contracts are wired. + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 19f9c51e7..3be9bbe9c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -23,6 +23,7 @@ public sealed class DrawingCanvas : IDisposable private readonly Configuration configuration; private readonly IDrawingBackend backend; private readonly ICanvasFrame targetFrame; + private readonly DrawingCanvasBatcher batcher; private bool isDisposed; /// @@ -58,6 +59,7 @@ internal DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); + this.batcher = new DrawingCanvasBatcher(configuration, backend, targetFrame); } /// @@ -98,7 +100,19 @@ public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOp this.EnsureNotDisposed(); Guard.NotNull(brush, nameof(brush)); Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); - this.backend.FillRegion(this.configuration, this.targetFrame, brush, graphicsOptions, region); + + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + RasterizerOptions rasterizerOptions = new( + region, + IntersectionRule.NonZero, + rasterizationMode, + RasterizerSamplingOrigin.PixelBoundary); + + RectangularPolygon regionPath = new(region.X, region.Y, region.Width, region.Height); + this.batcher.AddComposition(CompositionCommand.Create(regionPath, brush, graphicsOptions, rasterizerOptions)); } /// @@ -144,7 +158,14 @@ internal void FillPath( rasterizationMode, samplingOrigin); - this.backend.FillPath(this.configuration, this.targetFrame, path, brush, graphicsOptions, rasterizerOptions); + this.backend.FillPath( + this.configuration, + this.targetFrame, + path, + brush, + graphicsOptions, + rasterizerOptions, + this.batcher); } /// @@ -215,66 +236,26 @@ private void DrawTextOperations(IEnumerable operations, Drawin Guard.NotNull(operations, nameof(operations)); Guard.NotNull(drawingOptions, nameof(drawingOptions)); - Dictionary coverageCache = []; - this.backend.BeginCompositeSession(this.configuration, this.targetFrame); - try + foreach (DrawingOperation operation in operations.OrderBy(x => x.RenderPass)) { - // Operations are layered by render pass (fill, outline, decorations). - foreach (DrawingOperation operation in operations.OrderBy(x => x.RenderPass)) + if (!TryCreateCompositionCommand(operation, drawingOptions, out CompositionCommand composition)) { - Brush? compositeBrush = GetCompositeBrush(operation); - if (compositeBrush is null) - { - continue; - } - - GraphicsOptions graphicsOptions = - drawingOptions.GraphicsOptions.CloneOrReturnForRules( - operation.PixelAlphaCompositionMode, - operation.PixelColorBlendingMode); - bool useFallbackCoverage = !this.backend.SupportsCoverageComposition(compositeBrush, graphicsOptions); - - if (!this.TryGetCoverage( - operation, - drawingOptions, - useFallbackCoverage, - coverageCache, - out CoverageCacheEntry coverageEntry, - out Point coverageLocation)) - { - continue; - } - - if (!this.TryGetCompositeRegion( - coverageLocation, - coverageEntry.RasterizedSize, - out Rectangle compositeRegion, - out Point sourceOffset)) - { - continue; - } - - this.backend.CompositeCoverage( - this.configuration, - new CanvasRegionFrame(this.targetFrame, compositeRegion), - coverageEntry.CoverageHandle, - sourceOffset, - compositeBrush, - graphicsOptions, - this.Bounds); + continue; } - } - finally - { - this.backend.EndCompositeSession(this.configuration, this.targetFrame); - foreach ((_, CoverageCacheEntry coverageEntry) in coverageCache) - { - this.backend.ReleaseCoverage(coverageEntry.CoverageHandle); - } + this.batcher.AddComposition(composition); } } + /// + /// Flushes queued drawing commands to the target in submission order. + /// + public void Flush() + { + this.EnsureNotDisposed(); + this.batcher.FlushCompositions(); + } + /// public void Dispose() { @@ -283,92 +264,13 @@ public void Dispose() return; } + this.batcher.FlushCompositions(); this.isDisposed = true; } private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); - private bool TryGetCoverage( - DrawingOperation operation, - DrawingOptions drawingOptions, - bool useFallbackCoverage, - Dictionary coverageCache, - out CoverageCacheEntry coverageEntry, - out Point coverageLocation) - { - coverageLocation = operation.RenderLocation; - if (!TryCreateCoveragePath(operation, out IPath? coveragePath)) - { - coverageEntry = default; - return false; - } - - Point localOffset = Point.Empty; - if (operation.Kind == DrawingOperationKind.Draw) - { - int strokeHalf = (int)((operation.Pen?.StrokeWidth ?? 0F) / 2F); - coverageLocation = operation.RenderLocation - new Size(strokeHalf, strokeHalf); - - Point coverageMapOrigin = Point.Truncate(coveragePath.Bounds.Location); - localOffset = new Point( - coverageMapOrigin.X - operation.RenderLocation.X, - coverageMapOrigin.Y - operation.RenderLocation.Y); - coveragePath = coveragePath.Translate(-coverageMapOrigin); - } - - OperationCoverageCacheKey cacheKey = CreateOperationCoverageCacheKey(operation, localOffset, useFallbackCoverage); - if (coverageCache.TryGetValue(cacheKey, out coverageEntry)) - { - return true; - } - - Size rasterizedSize = Rectangle.Ceiling(coveragePath.Bounds).Size + new Size(2, 2); - if (rasterizedSize.Width <= 0 || rasterizedSize.Height <= 0) - { - coverageEntry = default; - return false; - } - - RasterizationMode rasterizationMode = drawingOptions.GraphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - RasterizerSamplingOrigin samplingOrigin = operation.Kind == DrawingOperationKind.Draw - ? RasterizerSamplingOrigin.PixelCenter - : RasterizerSamplingOrigin.PixelBoundary; - - RasterizerOptions rasterizerOptions = new( - new Rectangle(0, 0, rasterizedSize.Width, rasterizedSize.Height), - operation.IntersectionRule, - rasterizationMode, - samplingOrigin); - - DrawingCoverageHandle coverageHandle = this.backend.PrepareCoverage( - coveragePath, - rasterizerOptions, - this.configuration.MemoryAllocator, - useFallbackCoverage ? CoveragePreparationMode.Fallback : CoveragePreparationMode.Default); - if (!coverageHandle.IsValid) - { - coverageEntry = default; - return false; - } - - coverageEntry = new CoverageCacheEntry(coverageHandle, rasterizedSize); - coverageCache.Add(cacheKey, coverageEntry); - return true; - } - - private static Brush? GetCompositeBrush(DrawingOperation operation) - { - if (operation.Kind == DrawingOperationKind.Fill) - { - return operation.Brush; - } - - return operation.Pen?.StrokeFill; - } - private static RichTextOptions ConfigureTextOptions(RichTextOptions options) { if (options.Path is not null && options.Origin != Vector2.Zero) @@ -385,110 +287,96 @@ private static RichTextOptions ConfigureTextOptions(RichTextOptions options) return options; } - private static bool TryCreateCoveragePath( + private static bool TryCreateCompositionCommand( DrawingOperation operation, - [NotNullWhen(true)] out IPath? coveragePath) + DrawingOptions drawingOptions, + out CompositionCommand composition) { - if (operation.Kind == DrawingOperationKind.Fill) + Brush? compositeBrush = operation.Kind == DrawingOperationKind.Fill + ? operation.Brush + : operation.Pen?.StrokeFill; + if (compositeBrush is null) { - coveragePath = operation.Path; - return true; + composition = default; + return false; } - if (operation.Kind == DrawingOperationKind.Draw && operation.Pen is not null) + GraphicsOptions graphicsOptions = + drawingOptions.GraphicsOptions.CloneOrReturnForRules( + operation.PixelAlphaCompositionMode, + operation.PixelColorBlendingMode); + + IPath translatedPath = operation.Path.Translate(operation.RenderLocation); + IPath compositionPath; + RasterizerSamplingOrigin samplingOrigin; + if (operation.Kind == DrawingOperationKind.Draw) { - IPath globalPath = operation.Path.Translate(operation.RenderLocation); - coveragePath = operation.Pen.GeneratePath(globalPath); - return true; + if (operation.Pen is null) + { + composition = default; + return false; + } + + compositionPath = operation.Pen.GeneratePath(translatedPath); + samplingOrigin = RasterizerSamplingOrigin.PixelCenter; + } + else + { + compositionPath = translatedPath; + samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; } - coveragePath = null; - return false; - } + RectangleF bounds = compositionPath.Bounds; + if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } - private bool TryGetCompositeRegion( - Point coverageLocation, - Size coverageSize, - out Rectangle compositeRegion, - out Point sourceOffset) - { - Rectangle destination = new(coverageLocation, coverageSize); - Rectangle clipped = Rectangle.Intersect(this.Bounds, destination); - if (clipped.Equals(Rectangle.Empty)) + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + if (interest.Width <= 0 || interest.Height <= 0) { - compositeRegion = default; - sourceOffset = default; + composition = default; return false; } - sourceOffset = new Point(clipped.X - destination.X, clipped.Y - destination.Y); - compositeRegion = clipped; - return true; - } + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + RasterizerOptions rasterizerOptions = new( + interest, + operation.IntersectionRule, + rasterizationMode, + samplingOrigin); - private static OperationCoverageCacheKey CreateOperationCoverageCacheKey( - DrawingOperation operation, - Point localOffset, - bool useFallbackCoverage) - { int definitionKey = operation.DefinitionKey > 0 ? operation.DefinitionKey - : CreateFallbackDefinitionKey(operation); - return new OperationCoverageCacheKey(definitionKey, localOffset, useFallbackCoverage); + : CreateFallbackDefinitionKey(operation, compositeBrush); + + composition = CompositionCommand.Create( + definitionKey, + compositionPath, + compositeBrush, + graphicsOptions, + rasterizerOptions); + return true; } - private static int CreateFallbackDefinitionKey(DrawingOperation operation) + private static int CreateFallbackDefinitionKey(DrawingOperation operation, Brush compositeBrush) { HashCode hash = default; hash.Add(RuntimeHelpers.GetHashCode(operation.Path)); hash.Add((int)operation.Kind); hash.Add((int)operation.IntersectionRule); - hash.Add(operation.Brush is null ? 0 : RuntimeHelpers.GetHashCode(operation.Brush)); - hash.Add(operation.Pen is null ? 0 : RuntimeHelpers.GetHashCode(operation.Pen)); - return hash.ToHashCode(); - } - - private readonly struct CoverageCacheEntry - { - public CoverageCacheEntry(DrawingCoverageHandle coverageHandle, Size rasterizedSize) + hash.Add(RuntimeHelpers.GetHashCode(compositeBrush)); + if (operation.Pen is not null) { - this.CoverageHandle = coverageHandle; - this.RasterizedSize = rasterizedSize; + hash.Add(RuntimeHelpers.GetHashCode(operation.Pen)); } - public DrawingCoverageHandle CoverageHandle { get; } - - public Size RasterizedSize { get; } - } - - private readonly struct OperationCoverageCacheKey : IEquatable - { - private readonly int definitionKey; - private readonly Point localOffset; - private readonly bool useFallbackCoverage; - - public OperationCoverageCacheKey(int definitionKey, Point localOffset, bool useFallbackCoverage) - { - this.definitionKey = definitionKey; - this.localOffset = localOffset; - this.useFallbackCoverage = useFallbackCoverage; - } - - public bool Equals(OperationCoverageCacheKey other) - => this.definitionKey == other.definitionKey - && this.localOffset == other.localOffset - && this.useFallbackCoverage == other.useFallbackCoverage; - - public override bool Equals(object? obj) - => obj is OperationCoverageCacheKey other && this.Equals(other); - - public override int GetHashCode() - { - HashCode hash = default; - hash.Add(this.definitionKey); - hash.Add(this.localOffset); - hash.Add(this.useFallbackCoverage); - return hash.ToHashCode(); - } + return hash.ToHashCode(); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 470377a61..82e89c16f 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -188,7 +188,7 @@ public void ImageSharpCombinedPathsTiled() => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] - public void ImageSharpCombinedPathsWebGpuBackend() + public void ImageSharpCombinedPathsWebGPUBackend() => this.webGpuImage.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); [Benchmark] @@ -213,7 +213,7 @@ public void SkiaSharp() public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); [Benchmark] - public void FillPolygonWebGpuBackend() => this.webGpuImage.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); + public void FillPolygonWebGPUBackend() => this.webGpuImage.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); } public class DrawPolygonAll : DrawPolygon diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index d6f93d0d4..2ba22040c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -26,52 +26,43 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; - public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - } - - public void EndCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - } - public void FillPath( Configuration configuration, ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) + in RasterizerOptions rasterizerOptions, + DrawingCanvasBatcher batcher) where TPixel : unmanaged, IPixel - => DefaultDrawingBackend.Instance.FillPath( - configuration, - target, - path, - brush, - graphicsOptions, - rasterizerOptions); + => batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); - public void FillRegion( + public void FlushCompositions( Configuration configuration, ICanvasFrame target, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle region) - where TPixel : unmanaged, IPixel - => DefaultDrawingBackend.Instance.FillRegion( - configuration, - target, - brush, - graphicsOptions, - region); - - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) + IReadOnlyList compositions) where TPixel : unmanaged, IPixel { - ArgumentNullException.ThrowIfNull(brush); - _ = graphicsOptions; - return true; + for (int i = 0; i < compositions.Count; i++) + { + CompositionCommand composition = compositions[i]; + DrawingCoverageHandle coverage = this.PrepareCoverage( + composition.Path, + composition.RasterizerOptions, + configuration.MemoryAllocator, + CoveragePreparationMode.Default); + + this.CompositeCoverage( + configuration, + target, + coverage, + Point.Empty, + composition.Brush, + composition.GraphicsOptions, + composition.BrushBounds); + + this.ReleaseCoverage(coverage); + } } public DrawingCoverageHandle PrepareCoverage( diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 82ba75673..e9cfc15ed 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -39,10 +39,10 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImagePro Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); Assert.Equal(0, backend.LiveCoverageCount); AssertCoverageExecutionAccounting(backend); - if (backend.IsGpuReady) + if (backend.IsGPUReady) { - Assert.True(backend.GpuPrepareCoverageCallCount > 0); - Assert.True(backend.GpuCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount > 0); + Assert.True(backend.GPUPrepareCoverageCallCount > 0); + Assert.True(backend.GPUCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount > 0); } ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); @@ -202,10 +202,10 @@ private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backe { Assert.Equal( backend.PrepareCoverageCallCount, - backend.GpuPrepareCoverageCallCount + backend.FallbackPrepareCoverageCallCount); + backend.GPUPrepareCoverageCallCount + backend.FallbackPrepareCoverageCallCount); Assert.Equal( backend.CompositeCoverageCallCount, - backend.GpuCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount); + backend.GPUCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount); } private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) @@ -221,14 +221,14 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) } Assert.True( - backend.IsGpuReady, - $"WebGPU initialization did not succeed. Reason='{backend.LastGpuInitializationFailure}'. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}, Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); + backend.IsGPUReady, + $"WebGPU initialization did not succeed. Reason='{backend.LastGPUInitializationFailure}'. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GPUPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}, Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GPUCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); Assert.True( - backend.GpuPrepareCoverageCallCount > 0, - $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GpuPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}"); + backend.GPUPrepareCoverageCallCount > 0, + $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GPUPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}"); Assert.True( - backend.GpuCompositeCoverageCallCount > 0, - $"No GPU composite calls were observed. Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GpuCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); + backend.GPUCompositeCoverageCallCount > 0, + $"No GPU composite calls were observed. Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GPUCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); Assert.Equal( 0, backend.FallbackPrepareCoverageCallCount); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index d206e6905..5b53a0316 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -109,69 +109,24 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { - public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - } - - public void EndCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - } - public void FillPath( Configuration configuration, ICanvasFrame target, IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) - where TPixel : unmanaged, IPixel - { - } - - public void FillRegion( - Configuration configuration, - ICanvasFrame target, - Brush brush, - GraphicsOptions graphicsOptions, - Rectangle region) - where TPixel : unmanaged, IPixel - { - } - - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) - where TPixel : unmanaged, IPixel - { - _ = brush; - _ = graphicsOptions; - return true; - } - - public DrawingCoverageHandle PrepareCoverage( - IPath path, in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - CoveragePreparationMode preparationMode) + DrawingCanvasBatcher batcher) + where TPixel : unmanaged, IPixel { - _ = preparationMode; - return default; } - public void CompositeCoverage( + public void FlushCompositions( Configuration configuration, ICanvasFrame target, - DrawingCoverageHandle coverageHandle, - Point sourceOffset, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) + IReadOnlyList compositions) where TPixel : unmanaged, IPixel { } - - public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) - { - } } } From f7a8bc4c0208acc754c0d6d87c5398060e179220 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 22 Feb 2026 23:04:23 +1000 Subject: [PATCH 008/136] Refactor GPU composition to instance-based batching B --- .../Shaders/CompositeCoverageShader.cs | 16 +- ...ebGPUDrawingBackend.NativeSurfaceTarget.cs | 145 ++++ .../WebGPUDrawingBackend.cs | 756 ++++++++---------- .../Processing/Backends/CompositionBatch.cs | 28 + .../Processing/Backends/CompositionCommand.cs | 85 +- .../Backends/CompositionCoverageDefinition.cs | 34 + .../Backends/DefaultDrawingBackend.cs | 107 ++- .../Processing/Backends/IDrawingBackend.cs | 4 +- .../Backends/PreparedCompositionCommand.cs | 49 ++ .../DrawingCanvasBatcher{TPixel}.cs | 117 ++- .../Processing/DrawingCanvas{TPixel}.cs | 78 +- .../Processors/Text/DrawingOperation.cs | 2 - .../Processors/Text/RichTextGlyphRenderer.cs | 98 --- .../Drawing/DrawTextRepeatedGlyphs.cs | 200 +++++ .../Backends/SkiaCoverageDrawingBackend.cs | 59 +- .../Processing/DrawingCanvasBatcherTests.cs | 76 ++ .../RasterizerDefaultsExtensionsTests.cs | 2 +- 17 files changed, 1176 insertions(+), 680 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs create mode 100644 tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs index 4a7dd541d..af450357d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs @@ -8,7 +8,7 @@ internal static class CompositeCoverageShader private static readonly byte[] CodeBytes = [ .. """ - struct CompositeParams { + struct CompositeInstanceData { source_offset_x: u32, source_offset_y: u32, destination_x: u32, @@ -34,15 +34,19 @@ struct CompositeParams { var coverage: texture_2d; @group(0) @binding(1) - var params: CompositeParams; + var instances: array; struct VertexOutput { @builtin(position) position: vec4, @location(0) local: vec2, + @location(1) @interpolate(flat) instance_index: u32, }; @vertex - fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + fn vs_main( + @builtin(vertex_index) vertex_index: u32, + @builtin(instance_index) instance_index: u32) -> VertexOutput { + let params = instances[instance_index]; var vertices = array, 6>( vec2(0.0, 0.0), vec2(f32(params.destination_width), 0.0), @@ -59,10 +63,11 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { var output: VertexOutput; output.position = vec4(ndc_x, ndc_y, 0.0, 1.0); output.local = local; + output.instance_index = instance_index; return output; } - fn sample_brush(_local: vec2) -> vec4 { + fn sample_brush(params: CompositeInstanceData, _local: vec2) -> vec4 { switch params.brush_kind { case 0u: { return params.solid_brush_color; @@ -75,6 +80,7 @@ fn sample_brush(_local: vec2) -> vec4 { @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let params = instances[input.instance_index]; let local_x = u32(floor(input.local.x)); let local_y = u32(floor(input.local.y)); let source = vec2( @@ -86,7 +92,7 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4 { discard; } - let brush = sample_brush(input.local); + let brush = sample_brush(params, input.local); if (brush.a <= 0.0) { discard; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs new file mode 100644 index 000000000..c80e737ae --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal sealed unsafe partial class WebGPUDrawingBackend +{ + internal bool TryCreateNativeSurfaceTarget( + int width, + int height, + bool isSrgb, + bool isPremultipliedAlpha, + [NotNullWhen(true)] out NativeSurface? surface, + out nint textureHandle, + out nint textureViewHandle) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) + { + surface = null; + textureHandle = 0; + textureViewHandle = 0; + return false; + } + + return this.TryCreateNativeSurfaceTarget( + TPixel.GetPixelTypeInfo(), + width, + height, + pixelHandler.TextureFormat, + isSrgb, + isPremultipliedAlpha, + out surface, + out textureHandle, + out textureViewHandle); + } + + internal bool TryCreateNativeSurfaceTarget( + PixelTypeInfo pixelType, + int width, + int height, + TextureFormat textureFormat, + bool isSrgb, + bool isPremultipliedAlpha, + [NotNullWhen(true)] out NativeSurface? surface, + out nint textureHandle, + out nint textureViewHandle) + { + this.ThrowIfDisposed(); + + surface = null; + textureHandle = 0; + textureViewHandle = 0; + + if (!this.IsGPUReady || width <= 0 || height <= 0) + { + return false; + } + + lock (this.gpuSync) + { + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return false; + } + + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* targetTexture = gpuState.Api.DeviceCreateTexture(gpuState.Device, in targetTextureDescriptor); + if (targetTexture is null) + { + return false; + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* targetView = gpuState.Api.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + this.ReleaseTextureLocked(targetTexture); + return false; + } + + textureHandle = (nint)targetTexture; + textureViewHandle = (nint)targetView; + + NativeSurface nativeSurface = new(pixelType); + nativeSurface.SetCapability(new WebGPUSurfaceCapability( + (nint)gpuState.Device, + (nint)gpuState.Queue, + textureHandle, + textureViewHandle, + textureFormat, + width, + height, + isSrgb, + isPremultipliedAlpha)); + + surface = nativeSurface; + return true; + } + } + + internal void ReleaseNativeSurfaceTarget(nint textureHandle, nint textureViewHandle) + { + if ((textureHandle == 0 && textureViewHandle == 0) || this.isDisposed) + { + return; + } + + lock (this.gpuSync) + { + if (textureViewHandle != 0) + { + this.ReleaseTextureViewLocked((TextureView*)textureViewHandle); + } + + if (textureHandle != 0) + { + this.ReleaseTextureLocked((Texture*)textureHandle); + } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index d495ff051..7c24c5f89 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -33,8 +34,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; - private const uint CompositeUniformAlignment = 256; - private const uint CompositeUniformBufferSize = 256 * 1024; + private const nuint CompositeInstanceBufferSize = 256 * 1024; private const int CallbackTimeoutMilliseconds = 10_000; private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; @@ -42,11 +42,10 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; private readonly object gpuSync = new(); - private readonly ConcurrentDictionary preparedCoverage = new(); + private readonly ConcurrentDictionary coverageCache = new(); private readonly DefaultDrawingBackend fallbackBackend; private WebGPURasterizer? coverageRasterizer; - private int nextCoverageHandleId; private bool isDisposed; private WebGPURuntime.Lease? runtimeLease; private WebGPU? webGPU; @@ -68,8 +67,9 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private Texture* compositeSessionTargetTexture; private TextureView* compositeSessionTargetView; private WgpuBuffer* compositeSessionReadbackBuffer; - private WgpuBuffer* compositeSessionUniformBuffer; - private uint compositeSessionUniformWriteOffset; + private WgpuBuffer* compositeSessionInstanceBuffer; + private nuint compositeSessionInstanceBufferCapacity; + private CompositeInstanceData[]? compositeSessionInstanceScratch; private CommandEncoder* compositeSessionCommandEncoder; private uint compositeSessionReadbackBytesPerRow; private ulong compositeSessionReadbackByteCount; @@ -78,6 +78,7 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private TextureFormat compositeSessionResourceTextureFormat; private bool compositeSessionRequiresReadback; private bool compositeSessionOwnsTargetView; + private int liveCoverageCount; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); private static readonly bool TraceEnabled = string.Equals( Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), @@ -134,7 +135,7 @@ private static void Trace(string message) public int FallbackCompositeCoverageCallCount { get; private set; } /// - /// Gets the number of released coverage handles. + /// Gets the number of completed prepared-coverage uses. /// public int ReleaseCoverageCallCount { get; private set; } @@ -154,9 +155,9 @@ private static void Trace(string message) public string? LastGPUInitializationFailure { get; private set; } /// - /// Gets the number of prepared coverage entries currently cached by handle. + /// Gets the number of live per-flush prepared coverage handles. /// - public int LiveCoverageCount => this.preparedCoverage.Count; + public int LiveCoverageCount => this.liveCoverageCount; /// /// Begins a composite session for a target region. @@ -276,141 +277,127 @@ public void FillPath( where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); - batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); + batcher.AddComposition( + CompositionCommand.Create( + path, + brush, + graphicsOptions, + rasterizerOptions, + target.Bounds.Location)); } /// public void FlushCompositions( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); - if (compositions.Count == 0) + if (compositionBatch.Commands.Count == 0) { return; } - CompositionCommand coverageDefinition = compositions[0]; - ICanvasFrame compositeFrame = new CanvasRegionFrame(target, coverageDefinition.RasterizerOptions.Interest); - bool useGPUPath = this.TryResolveGPUFlush(out CompositePixelRegistration pixelHandler); - bool openedCompositeSession = false; - DrawingCoverageHandle coverageHandle = default; - - if (useGPUPath) - { - if (this.compositeSessionDepth == 0) - { - this.compositeSessionDepth = 1; - this.compositeSessionGPUActive = false; - this.compositeSessionDirty = false; - this.compositeSessionCommands.Clear(); - - useGPUPath = this.ActivateCompositeSession(compositeFrame, pixelHandler); - openedCompositeSession = true; - } - else - { - useGPUPath = this.compositeSessionGPUActive; - } - } - - if (useGPUPath) + if (!this.TryBeginGPUFlush(target, out bool openedCompositeSession)) { - coverageHandle = this.PrepareCoverage( - coverageDefinition.Path, - coverageDefinition.RasterizerOptions, - configuration.MemoryAllocator, - CoveragePreparationMode.Default); - useGPUPath = coverageHandle.IsValid; + this.FlushCompositionsFallback(configuration, target, compositionBatch); + return; } - if (!useGPUPath) + CompositionCoverageDefinition definition = compositionBatch.Definition; + RasterizerOptions rasterizerOptions = definition.RasterizerOptions; + CoverageEntry? coverageEntry = this.PrepareCoverageEntry( + definition.Path, + in rasterizerOptions); + if (coverageEntry is null) { if (openedCompositeSession) { - this.EndCompositeSession(configuration, compositeFrame); + this.EndCompositeSession(configuration, target); } - this.FlushCompositionsFallback(configuration, target, compositions); + this.FlushCompositionsFallback(configuration, target, compositionBatch); return; } + this.liveCoverageCount++; + this.ReleaseCoverageCallCount++; try { - for (int i = 0; i < compositions.Count; i++) - { - CompositionCommand command = compositions[i]; - this.CompositeCoverage( - configuration, - compositeFrame, - coverageHandle, - Point.Empty, - command.Brush, - command.GraphicsOptions, - command.BrushBounds); + IReadOnlyList commands = compositionBatch.Commands; + Rectangle targetBounds = target.Bounds; + lock (this.gpuSync) + { + this.compositeSessionCommands.EnsureCapacity(this.compositeSessionCommands.Count + commands.Count); + for (int i = 0; i < commands.Count; i++) + { + PreparedCompositionCommand command = commands[i]; + this.CompositeCoverageCallCount++; + + if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) + { + throw new InvalidOperationException("Unsupported brush for WebGPU composition."); + } + + this.QueueCompositeCoverageLocked( + coverageEntry, + targetBounds, + command.DestinationRegion, + command.SourceOffset, + brushData, + command.GraphicsOptions.BlendPercentage); + + this.GPUCompositeCoverageCallCount++; + } } } finally { + this.liveCoverageCount--; + if (openedCompositeSession) { - this.EndCompositeSession(configuration, compositeFrame); + this.EndCompositeSession(configuration, target); } - - this.ReleaseCoverage(coverageHandle); - } - } - - /// - /// Determines whether this backend can composite coverage with the given brush/options. - /// - public bool SupportsCoverageComposition(Brush brush, in GraphicsOptions graphicsOptions) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(brush, nameof(brush)); - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler) || - !this.IsGPUReady) - { - return false; - } - - lock (this.gpuSync) - { - return this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _); } } private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { + this.PrepareCoverageCallCount++; + this.FallbackPrepareCoverageCallCount++; + this.ReleaseCoverageCallCount++; + this.CompositeCoverageCallCount += compositionBatch.Commands.Count; + this.FallbackCompositeCoverageCallCount += compositionBatch.Commands.Count; + if (target.TryGetCpuRegion(out _)) { - this.fallbackBackend.FlushCompositions(configuration, target, compositions); + this.fallbackBackend.FlushCompositions(configuration, target, compositionBatch); return; } + if (!TryGetNativeSurfaceCapability( + target, + expectedTargetFormat: null, + requireWritableTexture: true, + out WebGPUSurfaceCapability? surfaceCapability)) + { + throw new NotSupportedException( + "Fallback composition requires either a CPU destination region or a native WebGPU surface exposing a writable texture handle."); + } + Rectangle targetBounds = target.Bounds; using Buffer2D stagingBuffer = configuration.MemoryAllocator.Allocate2D( new Size(targetBounds.Width, targetBounds.Height), AllocationOptions.Clean); Buffer2DRegion stagingRegion = new(stagingBuffer, targetBounds); - CpuCanvasFrame stagingFrame = new(stagingRegion); - this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositions); - - if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || - nativeSurface is null || - !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability) || - surfaceCapability is null || - surfaceCapability.TargetTexture == 0) - { - throw new NotSupportedException( - "Fallback composition requires either a CPU destination region or a native WebGPU surface exposing a writable texture handle."); - } + ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); + this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionBatch); lock (this.gpuSync) { @@ -422,142 +409,116 @@ surfaceCapability is null || } } - private bool TryResolveGPUFlush(out CompositePixelRegistration pixelHandler) + private bool TryBeginGPUFlush(ICanvasFrame target, out bool openedCompositeSession) where TPixel : unmanaged, IPixel { - pixelHandler = default; - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out pixelHandler) || - !this.IsGPUReady) + openedCompositeSession = false; + if (this.compositeSessionDepth > 0) + { + return this.compositeSessionGPUActive; + } + + if (!this.IsGPUReady || + !CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) { return false; } lock (this.gpuSync) { - return this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _); + if (!this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _)) + { + return false; + } } + + this.compositeSessionDepth = 1; + this.compositeSessionGPUActive = false; + this.compositeSessionDirty = false; + this.compositeSessionCommands.Clear(); + if (!this.ActivateCompositeSession(target, pixelHandler)) + { + this.compositeSessionDepth = 0; + this.compositeSessionGPUActive = false; + this.compositeSessionDirty = false; + this.compositeSessionCommands.Clear(); + return false; + } + + openedCompositeSession = true; + return true; } - /// - /// Prepares coverage for a path and returns an opaque reusable handle. - /// - /// - /// GPU preparation flattens path edges into local-interest coordinates, builds a tiled edge index, - /// and rasterizes the coverage texture. When GPU preparation is unavailable this returns an invalid handle. - /// - public DrawingCoverageHandle PrepareCoverage( + private CoverageEntry? PrepareCoverageEntry( IPath path, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - CoveragePreparationMode preparationMode) + in RasterizerOptions rasterizerOptions) { this.ThrowIfDisposed(); Guard.NotNull(path, nameof(path)); - _ = allocator; - _ = preparationMode; this.PrepareCoverageCallCount++; - Size size = rasterizerOptions.Interest.Size; - - Texture* coverageTexture = null; - TextureView* coverageView = null; - lock (this.gpuSync) - { - WebGPURasterizer? rasterizer = this.coverageRasterizer; - if (rasterizer is null) - { - this.FallbackPrepareCoverageCallCount++; - return default; - } - - if (!rasterizer.TryCreateCoverageTexture(path, in rasterizerOptions, out coverageTexture, out coverageView)) - { - this.FallbackPrepareCoverageCallCount++; - return default; - } - } - - int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); - CoverageEntry entry = new(size.Width, size.Height) + int definitionKey = CompositionCommand.ComputeCoverageDefinitionKey(path, in rasterizerOptions); + CoverageEntry? entry = this.GetOrCreateCoverageEntry(definitionKey, path, in rasterizerOptions); + if (entry is null) { - GPUCoverageTexture = coverageTexture, - GPUCoverageView = coverageView - }; - - if (!this.preparedCoverage.TryAdd(handleId, entry)) - { - lock (this.gpuSync) - { - this.ReleaseCoverageTextureLocked(entry); - } - - entry.Dispose(); - throw new InvalidOperationException("Failed to cache prepared coverage."); + this.FallbackPrepareCoverageCallCount++; + return null; } this.GPUPrepareCoverageCallCount++; - return new DrawingCoverageHandle(handleId); + return entry; } - /// - /// Composes prepared coverage into a target region using the provided brush. - /// - /// - /// Coverage handles are GPU-prepared and must be composed on the active GPU session. - /// - public void CompositeCoverage( - Configuration configuration, - ICanvasFrame target, - DrawingCoverageHandle coverageHandle, - Point sourceOffset, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) - where TPixel : unmanaged, IPixel + private CoverageEntry? GetOrCreateCoverageEntry( + int definitionKey, + IPath path, + in RasterizerOptions rasterizerOptions) { - this.ThrowIfDisposed(); - this.CompositeCoverageCallCount++; + if (this.coverageCache.TryGetValue(definitionKey, out CoverageEntry? cached)) + { + return cached; + } - if (!WebGPUBrushData.TryCreate(brush, brushBounds, out WebGPUBrushData brushData)) + CoverageEntry? created = this.CreateCoverageEntry(path, in rasterizerOptions); + if (created is null) { - throw new InvalidOperationException("Unsupported brush for WebGPU composition."); + return null; } - if (!this.TryCompositeCoverageGPU( - target, - coverageHandle, - sourceOffset, - brushData, - graphicsOptions.BlendPercentage)) + CoverageEntry winner = this.coverageCache.GetOrAdd(definitionKey, created); + if (!ReferenceEquals(winner, created)) { - throw new InvalidOperationException( - "Accelerated coverage composition failed for a handle prepared for accelerated mode."); + lock (this.gpuSync) + { + this.ReleaseCoverageTextureLocked(created); + } + + created.Dispose(); } - this.GPUCompositeCoverageCallCount++; + return winner; } - /// - /// Releases a previously prepared coverage handle. - /// - public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) + private CoverageEntry? CreateCoverageEntry(IPath path, in RasterizerOptions rasterizerOptions) { - this.ReleaseCoverageCallCount++; - if (!coverageHandle.IsValid) - { - return; - } - - Trace($"ReleaseCoverage: handle={coverageHandle.Value}"); - if (this.preparedCoverage.TryRemove(coverageHandle.Value, out CoverageEntry? entry)) + Texture* coverageTexture = null; + TextureView* coverageView = null; + lock (this.gpuSync) { - lock (this.gpuSync) + WebGPURasterizer? rasterizer = this.coverageRasterizer; + if (rasterizer is null || + !rasterizer.TryCreateCoverageTexture(path, in rasterizerOptions, out coverageTexture, out coverageView)) { - this.ReleaseCoverageTextureLocked(entry); + return null; } - - entry.Dispose(); } + + Size size = rasterizerOptions.Interest.Size; + return new CoverageEntry(size.Width, size.Height) + { + GPUCoverageTexture = coverageTexture, + GPUCoverageView = coverageView + }; } /// @@ -575,14 +536,6 @@ public void Dispose() { this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); - - foreach (KeyValuePair kv in this.preparedCoverage) - { - this.ReleaseCoverageTextureLocked(kv.Value); - kv.Value.Dispose(); - } - - this.preparedCoverage.Clear(); this.ReleaseGPUResourcesLocked(); } @@ -605,8 +558,11 @@ private bool ActivateCompositeSession( pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); } - else if (TryGetNativeSurfaceCapability(target, pixelHandler.TextureFormat, out WebGPUSurfaceCapability? nativeSurfaceCapability) && - nativeSurfaceCapability is not null && + else if (TryGetNativeSurfaceCapability( + target, + expectedTargetFormat: pixelHandler.TextureFormat, + requireWritableTexture: false, + out WebGPUSurfaceCapability? nativeSurfaceCapability) && this.BeginCompositeSurfaceSessionCoreLocked(target, nativeSurfaceCapability)) { started = true; @@ -625,20 +581,29 @@ nativeSurfaceCapability is not null && [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetNativeSurfaceCapability( ICanvasFrame target, - TextureFormat expectedTargetFormat, - out WebGPUSurfaceCapability? capability) + TextureFormat? expectedTargetFormat, + bool requireWritableTexture, + [NotNullWhen(true)] out WebGPUSurfaceCapability? capability) where TPixel : unmanaged, IPixel { - if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || nativeSurface is null) + if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability)) { capability = null; return false; } - if (!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability) || - surfaceCapability is null || - surfaceCapability.TargetTextureView == 0 || - surfaceCapability.TargetFormat != expectedTargetFormat) + if (expectedTargetFormat is TextureFormat requiredFormat) + { + if (surfaceCapability.TargetTextureView == 0 || + surfaceCapability.TargetFormat != requiredFormat) + { + capability = null; + return false; + } + } + + if (requireWritableTexture && surfaceCapability.TargetTexture == 0) { capability = null; return false; @@ -851,9 +816,9 @@ private bool TryCreateCompositePipelineLocked() Visibility = ShaderStage.Vertex | ShaderStage.Fragment, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = true, - MinBindingSize = (ulong)Unsafe.SizeOf() + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (ulong)Unsafe.SizeOf() } }; @@ -1207,7 +1172,6 @@ private bool BeginCompositeSessionCoreLocked( this.compositeSessionTargetRectangle = target.Rectangle; this.compositeSessionRequiresReadback = true; this.compositeSessionOwnsTargetView = true; - this.compositeSessionUniformWriteOffset = 0; this.compositeSessionDirty = false; return true; } @@ -1254,9 +1218,8 @@ private bool BeginCompositeSurfaceSessionCoreLocked( this.compositeSessionOwnsTargetView = false; this.compositeSessionRequiresReadback = false; this.compositeSessionTargetRectangle = target.Bounds; - this.compositeSessionUniformWriteOffset = 0; this.compositeSessionDirty = false; - return this.TryEnsureCompositeSessionUniformBufferLocked(); + return true; } private bool EnsureCompositeSessionResourcesLocked( @@ -1273,12 +1236,13 @@ private bool EnsureCompositeSessionResourcesLocked( if (this.compositeSessionTargetTexture is not null && this.compositeSessionTargetView is not null && this.compositeSessionReadbackBuffer is not null && - this.compositeSessionUniformBuffer is not null && this.compositeSessionResourceWidth == width && this.compositeSessionResourceHeight == height && this.compositeSessionResourceTextureFormat == textureFormat) { - return true; + return this.TryEnsureCompositeSessionInstanceBufferCapacityLocked( + in gpuState, + (nuint)Unsafe.SizeOf()); } this.ReleaseCompositeSessionResourcesLocked(); @@ -1335,26 +1299,9 @@ this.compositeSessionUniformBuffer is not null && return false; } - BufferDescriptor uniformBufferDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = CompositeUniformBufferSize - }; - - WgpuBuffer* uniformBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in uniformBufferDescriptor); - if (uniformBuffer is null) - { - this.ReleaseBufferLocked(readbackBuffer); - this.ReleaseTextureViewLocked(targetView); - this.ReleaseTextureLocked(targetTexture); - return false; - } - this.compositeSessionTargetTexture = targetTexture; this.compositeSessionTargetView = targetView; this.compositeSessionReadbackBuffer = readbackBuffer; - this.compositeSessionUniformBuffer = uniformBuffer; - this.compositeSessionUniformWriteOffset = 0; this.compositeSessionReadbackBytesPerRow = readbackRowBytes; this.compositeSessionReadbackByteCount = readbackByteCount; this.compositeSessionResourceWidth = width; @@ -1362,29 +1309,46 @@ this.compositeSessionUniformBuffer is not null && this.compositeSessionResourceTextureFormat = textureFormat; this.compositeSessionRequiresReadback = true; this.compositeSessionOwnsTargetView = true; - return true; + return this.TryEnsureCompositeSessionInstanceBufferCapacityLocked( + in gpuState, + (nuint)Unsafe.SizeOf()); } - private bool TryEnsureCompositeSessionUniformBufferLocked() + private bool TryEnsureCompositeSessionInstanceBufferCapacityLocked(in GPUState gpuState, nuint requiredBytes) { - if (this.compositeSessionUniformBuffer is not null) + if (requiredBytes == 0) { return true; } - if (!this.TryGetGPUState(out GPUState gpuState)) + if (this.compositeSessionInstanceBuffer is not null && + this.compositeSessionInstanceBufferCapacity >= requiredBytes) { - return false; + return true; } - BufferDescriptor uniformBufferDescriptor = new() + this.ReleaseAllCoverageCompositeBindGroupsLocked(); + this.ReleaseBufferLocked(this.compositeSessionInstanceBuffer); + + nuint targetSize = requiredBytes > CompositeInstanceBufferSize + ? requiredBytes + : CompositeInstanceBufferSize; + + BufferDescriptor instanceBufferDescriptor = new() { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = CompositeUniformBufferSize + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = targetSize }; - this.compositeSessionUniformBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in uniformBufferDescriptor); - return this.compositeSessionUniformBuffer is not null; + this.compositeSessionInstanceBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in instanceBufferDescriptor); + if (this.compositeSessionInstanceBuffer is null) + { + this.compositeSessionInstanceBufferCapacity = 0; + return false; + } + + this.compositeSessionInstanceBufferCapacity = targetSize; + return true; } /// @@ -1582,7 +1546,7 @@ private void ReleaseCompositeSessionResourcesLocked() } this.ReleaseAllCoverageCompositeBindGroupsLocked(); - this.ReleaseBufferLocked(this.compositeSessionUniformBuffer); + this.ReleaseBufferLocked(this.compositeSessionInstanceBuffer); this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); if (this.compositeSessionOwnsTargetView) { @@ -1590,8 +1554,9 @@ private void ReleaseCompositeSessionResourcesLocked() } this.ReleaseTextureLocked(this.compositeSessionTargetTexture); - this.compositeSessionUniformBuffer = null; - this.compositeSessionUniformWriteOffset = 0; + this.compositeSessionInstanceBuffer = null; + this.compositeSessionInstanceBufferCapacity = 0; + this.compositeSessionInstanceScratch = null; this.compositeSessionReadbackBuffer = null; this.compositeSessionTargetTexture = null; this.compositeSessionTargetView = null; @@ -1605,79 +1570,27 @@ private void ReleaseCompositeSessionResourcesLocked() this.compositeSessionCommands.Clear(); } - private bool TryCompositeCoverageGPU( - ICanvasFrame target, - DrawingCoverageHandle coverageHandle, + private void QueueCompositeCoverageLocked( + CoverageEntry entry, + in Rectangle targetBounds, + in Rectangle destinationRegion, Point sourceOffset, WebGPUBrushData brushData, float blendPercentage) - where TPixel : unmanaged, IPixel { - if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out CoverageEntry? entry)) - { - throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); - } - - if (target.Bounds.Width <= 0 || target.Bounds.Height <= 0) - { - return true; - } - - if ((uint)sourceOffset.X >= (uint)entry.Width || (uint)sourceOffset.Y >= (uint)entry.Height) - { - return true; - } - - int compositeWidth = Math.Min(target.Bounds.Width, entry.Width - sourceOffset.X); - int compositeHeight = Math.Min(target.Bounds.Height, entry.Height - sourceOffset.Y); - if (compositeWidth <= 0 || compositeHeight <= 0) - { - return true; - } - - lock (this.gpuSync) - { - if (!this.compositeSessionGPUActive || - this.compositeSessionDepth <= 0 || - this.compositeSessionTargetView is null) - { - return false; - } + int destinationX = targetBounds.X + destinationRegion.X - this.compositeSessionTargetRectangle.X; + int destinationY = targetBounds.Y + destinationRegion.Y - this.compositeSessionTargetRectangle.Y; - if (!TryEnsureCoverageTextureLocked(entry)) - { - return false; - } - - int sessionTargetWidth = this.compositeSessionTargetRectangle.Width; - int sessionTargetHeight = this.compositeSessionTargetRectangle.Height; - int destinationX = target.Bounds.X - this.compositeSessionTargetRectangle.X; - int destinationY = target.Bounds.Y - this.compositeSessionTargetRectangle.Y; - if ((uint)destinationX >= (uint)sessionTargetWidth || - (uint)destinationY >= (uint)sessionTargetHeight) - { - return false; - } - - int sessionCompositeWidth = Math.Min(compositeWidth, sessionTargetWidth - destinationX); - int sessionCompositeHeight = Math.Min(compositeHeight, sessionTargetHeight - destinationY); - if (sessionCompositeWidth <= 0 || sessionCompositeHeight <= 0) - { - return true; - } - - this.compositeSessionCommands.Add(new GPUCompositeCommand( - coverageHandle.Value, - sourceOffset, - brushData, - blendPercentage, - destinationX, - destinationY, - sessionCompositeWidth, - sessionCompositeHeight)); - this.compositeSessionDirty = true; - return true; - } + this.compositeSessionCommands.Add(new GPUCompositeCommand( + entry, + sourceOffset, + brushData, + blendPercentage, + destinationX, + destinationY, + destinationRegion.Width, + destinationRegion.Height)); + this.compositeSessionDirty = true; } private bool TryDrainQueuedCompositeCommandsLocked() @@ -1687,7 +1600,12 @@ private bool TryDrainQueuedCompositeCommandsLocked() return true; } - if (!this.TryEnsureCompositeSessionCommandEncoderLocked()) + if (!this.TryGetGPUState(out GPUState gpuState)) + { + return false; + } + + if (!this.TryEnsureCompositeSessionCommandEncoderLocked(in gpuState)) { return false; } @@ -1701,29 +1619,64 @@ private bool TryDrainQueuedCompositeCommandsLocked() int sessionTargetWidth = this.compositeSessionTargetRectangle.Width; int sessionTargetHeight = this.compositeSessionTargetRectangle.Height; - for (int i = 0; i < this.compositeSessionCommands.Count; i++) + int i = 0; + while (i < this.compositeSessionCommands.Count) { - GPUCompositeCommand command = this.compositeSessionCommands[i]; - if (!this.preparedCoverage.TryGetValue(command.CoverageHandleValue, out CoverageEntry? entry) || - !TryEnsureCoverageTextureLocked(entry)) + GPUCompositeCommand firstCommand = this.compositeSessionCommands[i]; + CoverageEntry entry = firstCommand.Coverage; + + int runStart = i; + i++; + while (i < this.compositeSessionCommands.Count && + ReferenceEquals(this.compositeSessionCommands[i].Coverage, entry)) + { + i++; + } + + int runCount = i - runStart; + nuint instanceDataSize = (nuint)(runCount * Unsafe.SizeOf()); + if (!this.TryEnsureCompositeSessionInstanceBufferCapacityLocked(in gpuState, instanceDataSize)) { return false; } + Span instances = this.GetCompositeInstanceScratch(runCount); + for (int instanceIndex = 0; instanceIndex < runCount; instanceIndex++) + { + GPUCompositeCommand command = this.compositeSessionCommands[runStart + instanceIndex]; + instances[instanceIndex] = new CompositeInstanceData + { + SourceOffsetX = (uint)command.SourceOffset.X, + SourceOffsetY = (uint)command.SourceOffset.Y, + DestinationX = (uint)command.DestinationX, + DestinationY = (uint)command.DestinationY, + DestinationWidth = (uint)command.CompositeWidth, + DestinationHeight = (uint)command.CompositeHeight, + TargetWidth = (uint)sessionTargetWidth, + TargetHeight = (uint)sessionTargetHeight, + BrushKind = (uint)command.BrushData.Kind, + SolidBrushColor = command.BrushData.SolidColor, + BlendPercentage = command.BlendPercentage + }; + } + + fixed (CompositeInstanceData* instancePtr = instances) + { + gpuState.Api.QueueWriteBuffer( + gpuState.Queue, + this.compositeSessionInstanceBuffer, + 0, + instancePtr, + instanceDataSize); + } + if (!this.TryRunCompositePassLocked( + in gpuState, this.compositeSessionCommandEncoder, compositePipeline, entry, - command.SourceOffset, - command.BrushData, - command.BlendPercentage, this.compositeSessionTargetView, - sessionTargetWidth, - sessionTargetHeight, - command.DestinationX, - command.DestinationY, - command.CompositeWidth, - command.CompositeHeight)) + (uint)runCount)) { return false; } @@ -1733,16 +1686,22 @@ private bool TryDrainQueuedCompositeCommandsLocked() return true; } - private bool TryEnsureCompositeSessionCommandEncoderLocked() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Span GetCompositeInstanceScratch(int count) { - if (this.compositeSessionCommandEncoder is not null) + if (this.compositeSessionInstanceScratch is null || this.compositeSessionInstanceScratch.Length < count) { - return true; + this.compositeSessionInstanceScratch = new CompositeInstanceData[Math.Max(256, count)]; } - if (!this.TryGetGPUState(out GPUState gpuState)) + return this.compositeSessionInstanceScratch.AsSpan(0, count); + } + + private bool TryEnsureCompositeSessionCommandEncoderLocked(in GPUState gpuState) + { + if (this.compositeSessionCommandEncoder is not null) { - return false; + return true; } CommandEncoderDescriptor commandEncoderDescriptor = default; @@ -1780,36 +1739,22 @@ private void TryCloseCompositeSessionPassLocked() : null; } - private static bool TryEnsureCoverageTextureLocked(CoverageEntry entry) - { - if (entry.GPUCoverageTexture is not null && entry.GPUCoverageView is not null) - { - return true; - } - - return false; - } - private BindGroup* GetOrCreateCoverageBindGroupLocked( + in GPUState gpuState, CoverageEntry coverageEntry, - WgpuBuffer* uniformBuffer, - uint uniformDataSize) + WgpuBuffer* instanceBuffer, + nuint instanceBufferSize) { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return null; - } - if (this.compositeBindGroupLayout is null || coverageEntry.GPUCoverageView is null || - uniformBuffer is null || - uniformDataSize == 0) + instanceBuffer is null || + instanceBufferSize == 0) { return null; } if (coverageEntry.GPUCompositeBindGroup is not null && - coverageEntry.GPUCompositeUniformBuffer == uniformBuffer) + coverageEntry.GPUCompositeInstanceBuffer == instanceBuffer) { return coverageEntry.GPUCompositeBindGroup; } @@ -1825,9 +1770,9 @@ uniformBuffer is null || bindGroupEntries[1] = new BindGroupEntry { Binding = 1, - Buffer = uniformBuffer, + Buffer = instanceBuffer, Offset = 0, - Size = uniformDataSize + Size = instanceBufferSize }; BindGroupDescriptor bindGroupDescriptor = new() @@ -1844,7 +1789,7 @@ uniformBuffer is null || } coverageEntry.GPUCompositeBindGroup = bindGroup; - coverageEntry.GPUCompositeUniformBuffer = uniformBuffer; + coverageEntry.GPUCompositeInstanceBuffer = instanceBuffer; return bindGroup; } @@ -1852,90 +1797,41 @@ uniformBuffer is null || /// Executes one composition draw call into the session target texture. /// private bool TryRunCompositePassLocked( + in GPUState gpuState, CommandEncoder* commandEncoder, RenderPipeline* compositePipeline, CoverageEntry coverageEntry, - Point sourceOffset, - WebGPUBrushData brushData, - float blendPercentage, TextureView* targetView, - int targetWidth, - int targetHeight, - int destinationX, - int destinationY, - int compositeWidth, - int compositeHeight) + uint instanceCount) { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - if (compositePipeline is null || this.compositeBindGroupLayout is null || coverageEntry.GPUCoverageView is null || - targetView is null || - targetWidth <= 0 || - targetHeight <= 0) + targetView is null) { return false; } - if (compositeWidth <= 0 || compositeHeight <= 0) + if (instanceCount == 0) { return true; } - if (this.compositeSessionUniformBuffer is null) - { - return false; - } - - uint uniformDataSize = (uint)Unsafe.SizeOf(); - uint uniformStride = AlignTo256(uniformDataSize); - if (uniformStride == 0 || - this.compositeSessionUniformWriteOffset > CompositeUniformBufferSize || - this.compositeSessionUniformWriteOffset + uniformStride > CompositeUniformBufferSize) + if (this.compositeSessionInstanceBuffer is null) { return false; } - uint uniformOffset = this.compositeSessionUniformWriteOffset; - this.compositeSessionUniformWriteOffset += uniformStride; - - BindGroup* bindGroup = this.GetOrCreateCoverageBindGroupLocked(coverageEntry, this.compositeSessionUniformBuffer, uniformDataSize); + BindGroup* bindGroup = this.GetOrCreateCoverageBindGroupLocked( + in gpuState, + coverageEntry, + this.compositeSessionInstanceBuffer, + this.compositeSessionInstanceBufferCapacity); if (bindGroup is null) { return false; } - if (commandEncoder is null) - { - return false; - } - - CompositeParams parameters = new() - { - SourceOffsetX = (uint)sourceOffset.X, - SourceOffsetY = (uint)sourceOffset.Y, - DestinationX = (uint)destinationX, - DestinationY = (uint)destinationY, - DestinationWidth = (uint)compositeWidth, - DestinationHeight = (uint)compositeHeight, - TargetWidth = (uint)targetWidth, - TargetHeight = (uint)targetHeight, - BrushKind = (uint)brushData.Kind, - SolidBrushColor = brushData.SolidColor, - BlendPercentage = blendPercentage - }; - - gpuState.Api.QueueWriteBuffer( - gpuState.Queue, - this.compositeSessionUniformBuffer, - uniformOffset, - ref parameters, - (nuint)Unsafe.SizeOf()); - if (this.compositeSessionPassEncoder is null) { RenderPassColorAttachment colorAttachment = new() @@ -1960,12 +1856,9 @@ targetView is null || } } - uint dynamicOffset = uniformOffset; - uint* dynamicOffsets = &dynamicOffset; - gpuState.Api.RenderPassEncoderSetPipeline(this.compositeSessionPassEncoder, compositePipeline); - gpuState.Api.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 1, dynamicOffsets); - gpuState.Api.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, 1, 0, 0); + gpuState.Api.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 0, null); + gpuState.Api.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, instanceCount, 0, 0); return true; } @@ -2091,12 +1984,12 @@ private void ReleaseCoverageCompositeBindGroupLocked(CoverageEntry entry) } entry.GPUCompositeBindGroup = null; - entry.GPUCompositeUniformBuffer = null; + entry.GPUCompositeInstanceBuffer = null; } private void ReleaseAllCoverageCompositeBindGroupsLocked() { - foreach (KeyValuePair kv in this.preparedCoverage) + foreach (KeyValuePair kv in this.coverageCache) { this.ReleaseCoverageCompositeBindGroupLocked(kv.Value); } @@ -2197,6 +2090,14 @@ private void ReleaseGPUResourcesLocked() this.ResetCompositeSessionStateLocked(); this.ReleaseCompositeSessionResourcesLocked(); + foreach (KeyValuePair kv in this.coverageCache) + { + this.ReleaseCoverageTextureLocked(kv.Value); + kv.Value.Dispose(); + } + + this.coverageCache.Clear(); + if (this.webGPU is not null) { this.coverageRasterizer?.Release(); @@ -2259,6 +2160,7 @@ private void ReleaseGPUResourcesLocked() this.wgpuExtension = null; this.runtimeLease?.Dispose(); this.runtimeLease = null; + this.liveCoverageCount = 0; this.IsGPUReady = false; this.compositeSessionGPUActive = false; this.compositeSessionDepth = 0; @@ -2270,7 +2172,7 @@ private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); [StructLayout(LayoutKind.Sequential)] - private struct CompositeParams + private struct CompositeInstanceData { public uint SourceOffsetX; public uint SourceOffsetY; @@ -2295,7 +2197,7 @@ private struct CompositeParams private readonly struct GPUCompositeCommand { public GPUCompositeCommand( - int coverageHandleValue, + CoverageEntry coverage, Point sourceOffset, WebGPUBrushData brushData, float blendPercentage, @@ -2304,7 +2206,7 @@ public GPUCompositeCommand( int compositeWidth, int compositeHeight) { - this.CoverageHandleValue = coverageHandleValue; + this.Coverage = coverage; this.SourceOffset = sourceOffset; this.BrushData = brushData; this.BlendPercentage = blendPercentage; @@ -2314,7 +2216,7 @@ public GPUCompositeCommand( this.CompositeHeight = compositeHeight; } - public int CoverageHandleValue { get; } + public CoverageEntry Coverage { get; } public Point SourceOffset { get; } @@ -2365,7 +2267,7 @@ public CoverageEntry(int width, int height) public BindGroup* GPUCompositeBindGroup { get; set; } - public WgpuBuffer* GPUCompositeUniformBuffer { get; set; } + public WgpuBuffer* GPUCompositeInstanceBuffer { get; set; } public void Dispose() { diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs new file mode 100644 index 000000000..7db484b26 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Prepared composition data emitted by and consumed by backends. +/// +internal sealed class CompositionBatch +{ + public CompositionBatch( + CompositionCoverageDefinition definition, + IReadOnlyList commands) + { + this.Definition = definition; + this.Commands = commands; + } + + /// + /// Gets the coverage definition that should be rasterized once per flush. + /// + public CompositionCoverageDefinition Definition { get; } + + /// + /// Gets normalized composition commands in original draw order. + /// + public IReadOnlyList Commands { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index 26887b181..1024ceb14 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -20,24 +19,23 @@ internal readonly struct CompositionCommand /// Brush bounds used for applicator creation. /// Graphics options used for composition. /// Rasterizer options used to generate coverage. + /// Absolute destination offset where coverage is composited. private CompositionCommand( int definitionKey, IPath path, Brush brush, Rectangle brushBounds, GraphicsOptions graphicsOptions, - RasterizerOptions rasterizerOptions) + RasterizerOptions rasterizerOptions, + Point destinationOffset) { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(brush, nameof(brush)); - Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); - this.DefinitionKey = definitionKey; this.Path = path; this.Brush = brush; this.BrushBounds = brushBounds; this.GraphicsOptions = graphicsOptions; this.RasterizerOptions = rasterizerOptions; + this.DestinationOffset = destinationOffset; } /// @@ -71,57 +69,38 @@ private CompositionCommand( public RasterizerOptions RasterizerOptions { get; } /// - /// Creates a composition command and computes a stable definition key from path/brush/rasterizer options. + /// Gets the absolute destination offset where the local coverage should be composited. /// - /// Path to rasterize in target-local coordinates. - /// Brush used during composition. - /// Graphics options used for composition. - /// Rasterizer options used to generate coverage. - /// The normalized composition command. - public static CompositionCommand Create( - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) - { - HashCode hash = default; - hash.Add(RuntimeHelpers.GetHashCode(path)); - hash.Add(RuntimeHelpers.GetHashCode(brush)); - hash.Add(rasterizerOptions.Interest); - hash.Add((int)rasterizerOptions.IntersectionRule); - hash.Add((int)rasterizerOptions.RasterizationMode); - hash.Add((int)rasterizerOptions.SamplingOrigin); - - return Create( - hash.ToHashCode(), - path, - brush, - graphicsOptions, - rasterizerOptions); - } + public Point DestinationOffset { get; } /// - /// Creates a composition command using a caller-provided definition key. + /// Creates a composition command and computes a stable definition key from path geometry and rasterizer options. /// - /// Stable definition key used for composition-level caching. /// Path to rasterize in target-local coordinates. /// Brush used during composition. /// Graphics options used for composition. /// Rasterizer options used to generate coverage. + /// Absolute destination offset where coverage is composited. /// The normalized composition command. public static CompositionCommand Create( - int definitionKey, IPath path, Brush brush, GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions) + in RasterizerOptions rasterizerOptions, + Point destinationOffset = default) { + int definitionKey = ComputeCoverageDefinitionKey(path, rasterizerOptions); RectangleF bounds = path.Bounds; - Rectangle brushBounds = Rectangle.FromLTRB( + Rectangle localBrushBounds = Rectangle.FromLTRB( (int)MathF.Floor(bounds.Left), (int)MathF.Floor(bounds.Top), (int)MathF.Ceiling(bounds.Right), (int)MathF.Ceiling(bounds.Bottom)); + Rectangle brushBounds = new( + localBrushBounds.X + destinationOffset.X, + localBrushBounds.Y + destinationOffset.Y, + localBrushBounds.Width, + localBrushBounds.Height); return new( definitionKey, @@ -129,6 +108,34 @@ public static CompositionCommand Create( brush, brushBounds, graphicsOptions, - rasterizerOptions); + rasterizerOptions, + destinationOffset); + } + + /// + /// Computes a coverage definition key from path geometry and rasterization state. + /// + /// Path to rasterize. + /// Rasterizer options used for coverage generation. + /// A stable key for coverage-equivalent commands. + public static int ComputeCoverageDefinitionKey(IPath path, in RasterizerOptions rasterizerOptions) + { + HashCode hash = default; + foreach (ISimplePath simplePath in path.Flatten()) + { + ReadOnlySpan points = simplePath.Points.Span; + hash.Add(points.Length); + for (int i = 0; i < points.Length; i++) + { + hash.Add(points[i].X); + hash.Add(points[i].Y); + } + } + + hash.Add(rasterizerOptions.Interest.Size); + hash.Add((int)rasterizerOptions.IntersectionRule); + hash.Add((int)rasterizerOptions.RasterizationMode); + hash.Add((int)rasterizerOptions.SamplingOrigin); + return hash.ToHashCode(); } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs new file mode 100644 index 000000000..3bb609749 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One coverage definition that can be rasterized once and reused by multiple composition commands. +/// +internal readonly struct CompositionCoverageDefinition +{ + public CompositionCoverageDefinition(int definitionKey, IPath path, in RasterizerOptions rasterizerOptions) + { + this.DefinitionKey = definitionKey; + this.Path = path; + this.RasterizerOptions = rasterizerOptions; + } + + /// + /// Gets the stable key for this coverage definition. + /// + public int DefinitionKey { get; } + + /// + /// Gets the path used to generate coverage. + /// + public IPath Path { get; } + + /// + /// Gets the rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 811f85cbd..42d5502f9 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Collections.Concurrent; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -11,6 +12,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed class DefaultDrawingBackend : IDrawingBackend { + private readonly ConcurrentDictionary> coverageCache = new(); + /// /// Initializes a new instance of the class. /// @@ -52,55 +55,99 @@ public void FillPath( in RasterizerOptions rasterizerOptions, DrawingCanvasBatcher batcher) where TPixel : unmanaged, IPixel - => batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); + => batcher.AddComposition( + CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); /// public void FlushCompositions( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { - _ = target.TryGetCpuRegion(out Buffer2DRegion destinationFrame); - - CompositionCommand coverageDefinition = compositions[0]; - using Buffer2D coverageMap = this.CreateCoverageMap(coverageDefinition, configuration.MemoryAllocator); - Buffer2DRegion destinationRegion = destinationFrame.GetSubRegion(coverageDefinition.RasterizerOptions.Interest); - - for (int row = 0; row < coverageMap.Height; row++) + if (compositionBatch.Commands.Count == 0) { - Span rowCoverage = coverageMap.DangerousGetRowSpan(row); - int y = destinationRegion.Rectangle.Y + row; + return; + } - for (int i = 0; i < compositions.Count; i++) + _ = target.TryGetCpuRegion(out Buffer2DRegion destinationFrame); + CompositionCoverageDefinition definition = compositionBatch.Definition; + Buffer2D coverageMap = this.GetOrCreateCoverageMap(definition, configuration.MemoryAllocator); + + Rectangle destinationBounds = destinationFrame.Rectangle; + IReadOnlyList commands = compositionBatch.Commands; + int commandCount = commands.Count; + BrushApplicator[] applicators = new BrushApplicator[commandCount]; + try + { + int maxHeight = 0; + for (int i = 0; i < commandCount; i++) { - CompositionCommand command = compositions[i]; - - // TODO: This should be optimized to avoid creating multiple applicators - // for the same brush/graphics options. - // We should create them first outside of the loop then dispose after. - using BrushApplicator applicator = command.Brush.CreateApplicator( + PreparedCompositionCommand command = commands[i]; + Buffer2DRegion commandRegion = destinationFrame.GetSubRegion(command.DestinationRegion); + applicators[i] = command.Brush.CreateApplicator( configuration, command.GraphicsOptions, - destinationRegion, + commandRegion, command.BrushBounds); - applicator.Apply(rowCoverage, destinationRegion.Rectangle.X, y); + if (command.DestinationRegion.Height > maxHeight) + { + maxHeight = command.DestinationRegion.Height; + } + } + + for (int row = 0; row < maxHeight; row++) + { + for (int i = 0; i < commandCount; i++) + { + PreparedCompositionCommand command = commands[i]; + if (row >= command.DestinationRegion.Height) + { + continue; + } + + int destinationX = destinationBounds.X + command.DestinationRegion.X; + int destinationY = destinationBounds.Y + command.DestinationRegion.Y; + int sourceStartX = command.SourceOffset.X; + int sourceStartY = command.SourceOffset.Y; + + Span rowCoverage = coverageMap.DangerousGetRowSpan(sourceStartY + row); + Span rowSlice = rowCoverage.Slice(sourceStartX, command.DestinationRegion.Width); + applicators[i].Apply(rowSlice, destinationX, destinationY + row); + } } } + finally + { + for (int i = 0; i < applicators.Length; i++) + { + applicators[i]?.Dispose(); + } + } + } + + private Buffer2D GetOrCreateCoverageMap( + in CompositionCoverageDefinition definition, + MemoryAllocator allocator) + { + CompositionCoverageDefinition localDefinition = definition; + return this.coverageCache.GetOrAdd( + localDefinition.DefinitionKey, + _ => this.CreateCoverageMap(localDefinition, allocator)); } private Buffer2D CreateCoverageMap( - CompositionCommand command, + in CompositionCoverageDefinition definition, MemoryAllocator allocator) { - Size size = command.RasterizerOptions.Interest.Size; + Size size = definition.RasterizerOptions.Interest.Size; Buffer2D coverage = allocator.Allocate2D(size, AllocationOptions.Clean); - (Buffer2D Buffer, int DestinationTop) state = (coverage, command.RasterizerOptions.Interest.Top); + (Buffer2D Buffer, int DestinationTop) state = (coverage, definition.RasterizerOptions.Interest.Top); this.PrimaryRasterizer.Rasterize( - command.Path, - command.RasterizerOptions, + definition.Path, + definition.RasterizerOptions, allocator, ref state, static (int y, Span scanline, ref (Buffer2D Buffer, int DestinationTop) callbackState) => @@ -111,4 +158,14 @@ private Buffer2D CreateCoverageMap( return coverage; } + + public void Dispose() + { + foreach (Buffer2D entry in this.coverageCache.Values) + { + entry.Dispose(); + } + + this.coverageCache.Clear(); + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 4c06e72ef..d23d3ca65 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -41,10 +41,10 @@ public void FillPath( /// The pixel format. /// Active processing configuration. /// Destination frame. - /// Queued composition commands in batch order. + /// Prepared composition definitions and commands in batch order. public void FlushCompositions( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs new file mode 100644 index 000000000..44ad6b175 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One normalized composition command that applies a brush to the active coverage map. +/// +internal readonly struct PreparedCompositionCommand +{ + public PreparedCompositionCommand( + Rectangle destinationRegion, + Point sourceOffset, + Brush brush, + Rectangle brushBounds, + GraphicsOptions graphicsOptions) + { + this.DestinationRegion = destinationRegion; + this.SourceOffset = sourceOffset; + this.Brush = brush; + this.BrushBounds = brushBounds; + this.GraphicsOptions = graphicsOptions; + } + + /// + /// Gets the destination region in target-local coordinates. + /// + public Rectangle DestinationRegion { get; } + + /// + /// Gets the source offset into the pre-rasterized coverage map. + /// + public Point SourceOffset { get; } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets graphics options used during composition. + /// + public GraphicsOptions GraphicsOptions { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index d1818cdf9..e9ba7485c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -5,9 +5,23 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +/// +/// Queues normalized composition commands emitted by +/// and flushes them to in deterministic draw order. +/// +/// +/// The batcher owns command buffering and normalization only; it does not rasterize or composite. +/// During flush it groups consecutive commands sharing the same coverage definition into a single +/// so backends rasterize once and apply multiple brushes in order. +/// internal sealed class DrawingCanvasBatcher where TPixel : unmanaged, IPixel { + private readonly Configuration configuration; + private readonly IDrawingBackend backend; + private readonly ICanvasFrame targetFrame; + private readonly List commands = []; + internal DrawingCanvasBatcher( Configuration configuration, IDrawingBackend backend, @@ -16,16 +30,109 @@ internal DrawingCanvasBatcher( Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); Guard.NotNull(targetFrame, nameof(targetFrame)); + + this.configuration = configuration; + this.backend = backend; + this.targetFrame = targetFrame; } + /// + /// Appends one normalized composition command to the pending queue. + /// + /// The command to queue. public void AddComposition(in CompositionCommand composition) - { - _ = composition; - // Stub: implementation is added after backend contracts are wired. - } + => this.commands.Add(composition); + /// + /// Flushes queued commands to the backend, preserving submission order. + /// + /// + /// This method performs only command normalization and grouping: + /// + /// Split the queue into contiguous runs of matching . + /// Clip each run command to the target frame bounds. + /// Compute so clipped destination pixels map to the correct coverage pixels. + /// Send one per contiguous run. + /// + /// The backend then rasterizes coverage once per batch definition and composites commands in order. + /// public void FlushCompositions() { - // Stub: implementation is added after backend contracts are wired. + if (this.commands.Count == 0) + { + return; + } + + try + { + Rectangle targetBounds = this.targetFrame.Bounds; + int index = 0; + while (index < this.commands.Count) + { + CompositionCommand definitionCommand = this.commands[index]; + int definitionKey = definitionCommand.DefinitionKey; + + // Build one batch for the contiguous run sharing the same coverage definition. + List preparedCommands = []; + for (; index < this.commands.Count && this.commands[index].DefinitionKey == definitionKey; index++) + { + CompositionCommand command = this.commands[index]; + Rectangle interest = command.RasterizerOptions.Interest; + Rectangle commandDestination = new( + command.DestinationOffset.X + interest.X, + command.DestinationOffset.Y + interest.Y, + interest.Width, + interest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, commandDestination); + + // Off-target commands in this run are dropped before backend dispatch. + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + continue; + } + + Rectangle destinationLocalRegion = new( + clippedDestination.X - targetBounds.X, + clippedDestination.Y - targetBounds.Y, + clippedDestination.Width, + clippedDestination.Height); + + Point sourceOffset = new( + clippedDestination.X - commandDestination.X, + clippedDestination.Y - commandDestination.Y); + + // Keep command ordering exactly as submitted. + preparedCommands.Add( + new PreparedCompositionCommand( + destinationLocalRegion, + sourceOffset, + command.Brush, + command.BrushBounds, + command.GraphicsOptions)); + } + + if (preparedCommands.Count == 0) + { + continue; + } + + CompositionCoverageDefinition definition = + new( + definitionKey, + definitionCommand.Path, + definitionCommand.RasterizerOptions); + + this.backend.FlushCompositions( + this.configuration, + this.targetFrame, + new CompositionBatch(definition, preparedCommands)); + } + } + finally + { + // Always clear the queue, even if backend flush throws. + this.commands.Clear(); + } } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 3be9bbe9c..019f03bf7 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -112,7 +110,13 @@ public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOp RasterizerSamplingOrigin.PixelBoundary); RectangularPolygon regionPath = new(region.X, region.Y, region.Width, region.Height); - this.batcher.AddComposition(CompositionCommand.Create(regionPath, brush, graphicsOptions, rasterizerOptions)); + this.batcher.AddComposition( + CompositionCommand.Create( + regionPath, + brush, + graphicsOptions, + rasterizerOptions, + this.targetFrame.Bounds.Location)); } /// @@ -238,12 +242,7 @@ private void DrawTextOperations(IEnumerable operations, Drawin foreach (DrawingOperation operation in operations.OrderBy(x => x.RenderPass)) { - if (!TryCreateCompositionCommand(operation, drawingOptions, out CompositionCommand composition)) - { - continue; - } - - this.batcher.AddComposition(composition); + this.batcher.AddComposition(this.CreateCompositionCommand(operation, drawingOptions)); } } @@ -287,42 +286,29 @@ private static RichTextOptions ConfigureTextOptions(RichTextOptions options) return options; } - private static bool TryCreateCompositionCommand( + private CompositionCommand CreateCompositionCommand( DrawingOperation operation, - DrawingOptions drawingOptions, - out CompositionCommand composition) + DrawingOptions drawingOptions) { - Brush? compositeBrush = operation.Kind == DrawingOperationKind.Fill - ? operation.Brush - : operation.Pen?.StrokeFill; - if (compositeBrush is null) - { - composition = default; - return false; - } + Brush compositeBrush = operation.Kind == DrawingOperationKind.Fill + ? operation.Brush! + : operation.Pen!.StrokeFill; GraphicsOptions graphicsOptions = drawingOptions.GraphicsOptions.CloneOrReturnForRules( operation.PixelAlphaCompositionMode, operation.PixelColorBlendingMode); - IPath translatedPath = operation.Path.Translate(operation.RenderLocation); IPath compositionPath; RasterizerSamplingOrigin samplingOrigin; if (operation.Kind == DrawingOperationKind.Draw) { - if (operation.Pen is null) - { - composition = default; - return false; - } - - compositionPath = operation.Pen.GeneratePath(translatedPath); + compositionPath = operation.Pen!.GeneratePath(operation.Path); samplingOrigin = RasterizerSamplingOrigin.PixelCenter; } else { - compositionPath = translatedPath; + compositionPath = operation.Path; samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; } @@ -337,46 +323,26 @@ private static bool TryCreateCompositionCommand( (int)MathF.Floor(bounds.Top), (int)MathF.Ceiling(bounds.Right), (int)MathF.Ceiling(bounds.Bottom)); - if (interest.Width <= 0 || interest.Height <= 0) - { - composition = default; - return false; - } RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + RasterizerOptions rasterizerOptions = new( interest, operation.IntersectionRule, rasterizationMode, samplingOrigin); - int definitionKey = operation.DefinitionKey > 0 - ? operation.DefinitionKey - : CreateFallbackDefinitionKey(operation, compositeBrush); + Point destinationOffset = new( + this.targetFrame.Bounds.X + operation.RenderLocation.X, + this.targetFrame.Bounds.Y + operation.RenderLocation.Y); - composition = CompositionCommand.Create( - definitionKey, + return CompositionCommand.Create( compositionPath, compositeBrush, graphicsOptions, - rasterizerOptions); - return true; - } - - private static int CreateFallbackDefinitionKey(DrawingOperation operation, Brush compositeBrush) - { - HashCode hash = default; - hash.Add(RuntimeHelpers.GetHashCode(operation.Path)); - hash.Add((int)operation.Kind); - hash.Add((int)operation.IntersectionRule); - hash.Add(RuntimeHelpers.GetHashCode(compositeBrush)); - if (operation.Pen is not null) - { - hash.Add(RuntimeHelpers.GetHashCode(operation.Pen)); - } - - return hash.ToHashCode(); + rasterizerOptions, + destinationOffset); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs index ef2d657b7..4ea3bbf03 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs @@ -11,8 +11,6 @@ internal enum DrawingOperationKind : byte internal struct DrawingOperation { - public int DefinitionKey { get; set; } - public DrawingOperationKind Kind { get; set; } public IPath Path { get; set; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index ec3b59b3e..25a60264e 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -41,8 +41,6 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa // - Cache hit ratio above 60% private const float AccuracyMultiple = 8; private readonly Dictionary> glyphCache = []; - private readonly Dictionary operationDefinitionCache = []; - private int nextOperationDefinitionKey = 1; private int cacheReadIndex; private bool rasterizationRequired; @@ -86,8 +84,6 @@ public RichTextGlyphRenderer( protected override void BeginText(in FontRectangle bounds) { this.DrawingOperations.Clear(); - this.operationDefinitionCache.Clear(); - this.nextOperationDefinitionKey = 1; } /// @@ -129,7 +125,6 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara this.currentCacheKey = CacheKey.FromParameters( parameters, new RectangleF(subPixelLocation, subPixelSize), - this.currentBrush ?? this.defaultBrush, this.currentPen ?? this.defaultPen); if (this.glyphCache.ContainsKey(this.currentCacheKey)) { @@ -242,12 +237,6 @@ protected override void EndLayer() IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { - DefinitionKey = this.GetOrCreateOperationDefinitionKey( - fillPath, - fillRule, - DrawingOperationKind.Fill, - this.currentBrush, - null), Kind = DrawingOperationKind.Fill, Path = fillPath, RenderLocation = renderLocation, @@ -375,12 +364,6 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star Brush decorationBrush = pen.StrokeFill; this.DrawingOperations.Add(new DrawingOperation { - DefinitionKey = this.GetOrCreateOperationDefinitionKey( - decorationPath, - IntersectionRule.NonZero, - DrawingOperationKind.Fill, - decorationBrush, - null), Kind = DrawingOperationKind.Fill, Path = decorationPath, RenderLocation = renderLocation, @@ -499,12 +482,6 @@ protected override void EndGlyph() IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { - DefinitionKey = this.GetOrCreateOperationDefinitionKey( - glyphPath, - fillRule, - DrawingOperationKind.Fill, - this.currentBrush, - null), Kind = DrawingOperationKind.Fill, Path = glyphPath, RenderLocation = renderLocation, @@ -521,12 +498,6 @@ protected override void EndGlyph() IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); this.DrawingOperations.Add(new DrawingOperation { - DefinitionKey = this.GetOrCreateOperationDefinitionKey( - glyphPath, - outlineRule, - DrawingOperationKind.Draw, - null, - this.currentPen), Kind = DrawingOperationKind.Draw, Path = glyphPath, RenderLocation = renderLocation, @@ -549,24 +520,6 @@ private void UpdateCache(GlyphRenderData renderData) this.glyphCache[this.currentCacheKey].Add(renderData); } - private int GetOrCreateOperationDefinitionKey( - IPath path, - IntersectionRule intersectionRule, - DrawingOperationKind kind, - Brush? brush, - Pen? pen) - { - OperationDefinitionCacheKey cacheKey = new(path, intersectionRule, kind, brush, pen); - if (this.operationDefinitionCache.TryGetValue(cacheKey, out int existing)) - { - return existing; - } - - int next = this.nextOperationDefinitionKey++; - this.operationDefinitionCache.Add(cacheKey, next); - return next; - } - public void Dispose() => this.Dispose(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -601,7 +554,6 @@ private void Dispose(bool disposing) if (disposing) { this.glyphCache.Clear(); - this.operationDefinitionCache.Clear(); this.DrawingOperations.Clear(); } @@ -609,50 +561,6 @@ private void Dispose(bool disposing) } } - private readonly struct OperationDefinitionCacheKey : IEquatable - { - private readonly IPath path; - private readonly IntersectionRule intersectionRule; - private readonly DrawingOperationKind kind; - private readonly Brush? brush; - private readonly Pen? pen; - - public OperationDefinitionCacheKey( - IPath path, - IntersectionRule intersectionRule, - DrawingOperationKind kind, - Brush? brush, - Pen? pen) - { - this.path = path; - this.intersectionRule = intersectionRule; - this.kind = kind; - this.brush = brush; - this.pen = pen; - } - - public bool Equals(OperationDefinitionCacheKey other) - => ReferenceEquals(this.path, other.path) - && this.intersectionRule == other.intersectionRule - && this.kind == other.kind - && ReferenceEquals(this.brush, other.brush) - && ReferenceEquals(this.pen, other.pen); - - public override bool Equals(object? obj) - => obj is OperationDefinitionCacheKey other && this.Equals(other); - - public override int GetHashCode() - { - HashCode hash = default; - hash.Add(RuntimeHelpers.GetHashCode(this.path)); - hash.Add((int)this.intersectionRule); - hash.Add((int)this.kind); - hash.Add(this.brush is null ? 0 : RuntimeHelpers.GetHashCode(this.brush)); - hash.Add(this.pen is null ? 0 : RuntimeHelpers.GetHashCode(this.pen)); - return hash.ToHashCode(); - } - } - private struct GlyphRenderData { public Vector2 LocationDelta; @@ -688,8 +596,6 @@ private struct GlyphRenderData public RectangleF Bounds { get; init; } - public Brush? BrushReference { get; init; } - public Pen? PenReference { get; init; } public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); @@ -699,7 +605,6 @@ private struct GlyphRenderData public static CacheKey FromParameters( in GlyphRendererParameters parameters, RectangleF bounds, - Brush? brushReference, Pen? penReference) => new() { @@ -717,7 +622,6 @@ public static CacheKey FromParameters( TextAttributes = parameters.TextRun.TextAttributes, TextDecorations = parameters.TextRun.TextDecorations, Bounds = bounds, - BrushReference = brushReference, PenReference = penReference }; @@ -738,7 +642,6 @@ public bool Equals(CacheKey other) this.TextAttributes == other.TextAttributes && this.TextDecorations == other.TextDecorations && this.Bounds.Equals(other.Bounds) && - ReferenceEquals(this.BrushReference, other.BrushReference) && ReferenceEquals(this.PenReference, other.PenReference); public override int GetHashCode() @@ -757,7 +660,6 @@ public override int GetHashCode() hash.Add(this.TextAttributes); hash.Add(this.TextDecorations); hash.Add(this.Bounds); - hash.Add(this.BrushReference is null ? 0 : RuntimeHelpers.GetHashCode(this.BrushReference)); hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference)); return hash.ToHashCode(); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs new file mode 100644 index 000000000..3981cb8cd --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -0,0 +1,200 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +[MemoryDiagnoser] +public class DrawTextRepeatedGlyphs +{ + public const int Width = 1200; + public const int Height = 280; + + private readonly DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true + } + }; + + private readonly GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + private readonly Brush brush = Brushes.Solid(Color.HotPink); + private readonly Brush clearBrush = Brushes.Solid(Color.Transparent); + + private Configuration defaultConfiguration; + private Image defaultImage; + private Image webGpuCpuImage; + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; + private NativeSurfaceOnlyFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; + private RichTextOptions textOptions; + private string text; + + [Params(200, 1000)] + public int GlyphCount { get; set; } + + [GlobalSetup] + public void Setup() + { + Font font = SystemFonts.CreateFont("Arial", 48); + this.textOptions = new RichTextOptions(font) + { + Origin = new PointF(8, 8), + WrappingLength = Width - 16 + }; + + this.defaultConfiguration = Configuration.Default; + this.defaultImage = new Image(Width, Height); + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + this.webGpuCpuImage = new Image(this.webGpuConfiguration, Width, Height); + + if (!this.webGpuBackend.TryCreateNativeSurfaceTarget( + Width, + Height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. GPUReady={this.webGpuBackend.IsGPUReady}, Error='{this.webGpuBackend.LastGPUInitializationFailure ?? ""}'."); + } + + this.webGpuNativeFrame = new NativeSurfaceOnlyFrame( + new Rectangle(0, 0, Width, Height), + nativeSurface); + + this.text = new string('A', this.GlyphCount); + } + + [IterationSetup(Target = nameof(DrawingCanvasDefaultBackend))] + public void IterationSetupDefault() + => this.ClearWithDrawingCanvas( + this.defaultConfiguration, + new CpuRegionOnlyFrame(GetFrameRegion(this.defaultImage))); + + [IterationSetup(Target = nameof(DrawingCanvasWebGPUBackendCpuRegion))] + public void IterationSetupWebGpuCpuRegion() + => this.ClearWithDrawingCanvas( + this.webGpuConfiguration, + new CpuRegionOnlyFrame(GetFrameRegion(this.webGpuCpuImage))); + + [IterationSetup(Target = nameof(DrawingCanvasWebGPUBackendNativeSurface))] + public void IterationSetupWebGpuNativeSurface() + => this.ClearWithDrawingCanvas( + this.webGpuConfiguration, + this.webGpuNativeFrame); + + [GlobalCleanup] + public void Cleanup() + { + this.defaultImage.Dispose(); + this.webGpuCpuImage.Dispose(); + this.webGpuBackend.ReleaseNativeSurfaceTarget(this.webGpuNativeTextureHandle, this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; + this.webGpuBackend.Dispose(); + } + + [Benchmark(Baseline = true, Description = "DrawingCanvas Default Backend")] + public void DrawingCanvasDefaultBackend() + { + using DrawingCanvas canvas = new(this.defaultConfiguration, new CpuRegionOnlyFrame(GetFrameRegion(this.defaultImage))); + canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + canvas.Flush(); + } + + [Benchmark(Description = "DrawingCanvas WebGPU Backend (CPURegion)")] + public void DrawingCanvasWebGPUBackendCpuRegion() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, new CpuRegionOnlyFrame(GetFrameRegion(this.webGpuCpuImage))); + canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + canvas.Flush(); + } + + [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] + public void DrawingCanvasWebGPUBackendNativeSurface() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame); + canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + canvas.Flush(); + } + + private void ClearWithDrawingCanvas(Configuration configuration, ICanvasFrame target) + { + using DrawingCanvas canvas = new(configuration, target); + canvas.Fill(this.clearBrush, this.clearOptions); + canvas.Flush(); + } + + private static Buffer2DRegion GetFrameRegion(Image image) + => new(image.Frames.RootFrame.PixelBuffer, new Rectangle(0, 0, image.Width, image.Height)); + + private sealed class CpuRegionOnlyFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly Buffer2DRegion region; + + public CpuRegionOnlyFrame(Buffer2DRegion region) => this.region = region; + + public Rectangle Bounds => this.region.Rectangle; + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = this.region; + return true; + } + + public bool TryGetNativeSurface(out NativeSurface surface) + { + surface = default; + return false; + } + } + + private sealed class NativeSurfaceOnlyFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly Rectangle bounds; + private readonly NativeSurface surface; + + public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) + { + this.bounds = bounds; + this.surface = surface; + } + + public Rectangle Bounds => this.bounds; + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + public bool TryGetNativeSurface(out NativeSurface surface) + { + surface = this.surface; + return true; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 2ba22040c..c59c690f1 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -35,33 +35,52 @@ public void FillPath( in RasterizerOptions rasterizerOptions, DrawingCanvasBatcher batcher) where TPixel : unmanaged, IPixel - => batcher.AddComposition(CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions)); + => batcher.AddComposition( + CompositionCommand.Create( + path, + brush, + graphicsOptions, + rasterizerOptions, + target.Bounds.Location)); public void FlushCompositions( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { - for (int i = 0; i < compositions.Count; i++) + if (compositionBatch.Commands.Count == 0) { - CompositionCommand composition = compositions[i]; - DrawingCoverageHandle coverage = this.PrepareCoverage( - composition.Path, - composition.RasterizerOptions, - configuration.MemoryAllocator, - CoveragePreparationMode.Default); - - this.CompositeCoverage( - configuration, - target, - coverage, - Point.Empty, - composition.Brush, - composition.GraphicsOptions, - composition.BrushBounds); - - this.ReleaseCoverage(coverage); + return; + } + + CompositionCoverageDefinition definition = compositionBatch.Definition; + DrawingCoverageHandle coverageHandle = this.PrepareCoverage( + definition.Path, + definition.RasterizerOptions, + configuration.MemoryAllocator, + CoveragePreparationMode.Default); + try + { + IReadOnlyList commands = compositionBatch.Commands; + for (int i = 0; i < commands.Count; i++) + { + PreparedCompositionCommand composition = commands[i]; + ICanvasFrame commandTarget = new CanvasRegionFrame(target, composition.DestinationRegion); + + this.CompositeCoverage( + configuration, + commandTarget, + coverageHandle, + composition.SourceOffset, + composition.Brush, + composition.GraphicsOptions, + composition.BrushBounds); + } + } + finally + { + this.ReleaseCoverage(coverageHandle); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs new file mode 100644 index 000000000..0ee8348ed --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class DrawingCanvasBatcherTests +{ + [Fact] + public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + using Image image = new(40, 40); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + using DrawingCanvas canvas = new(configuration, backend, new CpuCanvasFrame(region)); + + IPath path = new RectangularPolygon(4, 6, 18, 12); + DrawingOptions options = new(); + Brush brushA = Brushes.Solid(Color.Red); + Brush brushB = Brushes.Solid(Color.Blue); + + canvas.FillPath(path, brushA, options); + canvas.FillPath(path, brushB, options); + canvas.Flush(); + + Assert.True(backend.HasBatch); + Assert.NotNull(backend.LastBatch.Definition.Path); + Assert.Equal(2, backend.LastBatch.Commands.Count); + Assert.Same(brushA, backend.LastBatch.Commands[0].Brush); + Assert.Same(brushB, backend.LastBatch.Commands[1].Brush); + } + + private sealed class CapturingBackend : IDrawingBackend + { + public bool HasBatch { get; private set; } + + public CompositionBatch LastBatch { get; private set; } = new( + new CompositionCoverageDefinition( + 0, + EmptyPath.ClosedPath, + new RasterizerOptions( + Rectangle.Empty, + IntersectionRule.NonZero, + RasterizationMode.Aliased, + RasterizerSamplingOrigin.PixelBoundary)), + Array.Empty()); + + public void FillPath( + Configuration configuration, + ICanvasFrame target, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + DrawingCanvasBatcher batcher) + where TPixel : unmanaged, IPixel + => batcher.AddComposition( + CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); + + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionBatch compositionBatch) + where TPixel : unmanaged, IPixel + { + this.LastBatch = compositionBatch; + this.HasBatch = true; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 5b53a0316..8124edfaf 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -124,7 +124,7 @@ public void FillPath( public void FlushCompositions( Configuration configuration, ICanvasFrame target, - IReadOnlyList compositions) + CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { } From 1b3b692927cc8c0be2a5dedc32378ac9fd3638c5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 15:27:03 +1000 Subject: [PATCH 009/136] Refactor WebGPU backend to use FlushContext --- .../WebGPUDrawingBackend.CompositePixels.cs | 14 + ...ebGPUDrawingBackend.NativeSurfaceTarget.cs | 145 - .../WebGPUDrawingBackend.cs | 2375 +++-------------- .../WebGPUFlushContext.cs | 1321 +++++++++ .../WebGPUNativeSurfaceFactory.cs | 110 + .../WebGPURasterizer.cs | 59 +- .../WebGPURuntime.cs | 64 +- .../WebGPUSurfaceCapability.cs | 10 +- .../WebGPUTestNativeSurfaceAllocator.cs | 129 + .../WebGPUTextureFormatId.cs | 96 + .../WebGPUTextureFormatMapper.cs | 15 + .../Processing/Backends/CompositionBatch.cs | 18 +- .../Processing/Backends/CompositionCommand.cs | 6 +- .../Backends/DefaultDrawingBackend.cs | 20 +- .../DrawingCanvasBatcher{TPixel}.cs | 22 +- .../Processors/Text/RichTextGlyphRenderer.cs | 6 +- .../Processing/SolidBrush.cs | 8 +- .../Drawing/DrawTextRepeatedGlyphs.cs | 12 +- .../ImageSharp.Drawing.Tests.csproj | 2 +- .../Backends/WebGPUDrawingBackendTests.cs | 60 +- .../WebGPUTextureFormatMapperTests.cs | 44 + 21 files changed, 2341 insertions(+), 2195 deletions(-) delete mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index efc379b81..9f4c3d8c5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -43,6 +43,20 @@ private static Dictionary CreateCompositePixel [typeof(Rgba64)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Uint) }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration registration)) + { + formatId = default; + return false; + } + + formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); + return true; + } + private readonly struct CompositePixelRegistration { public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, int pixelSizeInBytes) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs deleted file mode 100644 index c80e737ae..000000000 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.NativeSurfaceTarget.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using Silk.NET.WebGPU; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal sealed unsafe partial class WebGPUDrawingBackend -{ - internal bool TryCreateNativeSurfaceTarget( - int width, - int height, - bool isSrgb, - bool isPremultipliedAlpha, - [NotNullWhen(true)] out NativeSurface? surface, - out nint textureHandle, - out nint textureViewHandle) - where TPixel : unmanaged, IPixel - { - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) - { - surface = null; - textureHandle = 0; - textureViewHandle = 0; - return false; - } - - return this.TryCreateNativeSurfaceTarget( - TPixel.GetPixelTypeInfo(), - width, - height, - pixelHandler.TextureFormat, - isSrgb, - isPremultipliedAlpha, - out surface, - out textureHandle, - out textureViewHandle); - } - - internal bool TryCreateNativeSurfaceTarget( - PixelTypeInfo pixelType, - int width, - int height, - TextureFormat textureFormat, - bool isSrgb, - bool isPremultipliedAlpha, - [NotNullWhen(true)] out NativeSurface? surface, - out nint textureHandle, - out nint textureViewHandle) - { - this.ThrowIfDisposed(); - - surface = null; - textureHandle = 0; - textureViewHandle = 0; - - if (!this.IsGPUReady || width <= 0 || height <= 0) - { - return false; - } - - lock (this.gpuSync) - { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - TextureDescriptor targetTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = textureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* targetTexture = gpuState.Api.DeviceCreateTexture(gpuState.Device, in targetTextureDescriptor); - if (targetTexture is null) - { - return false; - } - - TextureViewDescriptor targetViewDescriptor = new() - { - Format = textureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* targetView = gpuState.Api.TextureCreateView(targetTexture, in targetViewDescriptor); - if (targetView is null) - { - this.ReleaseTextureLocked(targetTexture); - return false; - } - - textureHandle = (nint)targetTexture; - textureViewHandle = (nint)targetView; - - NativeSurface nativeSurface = new(pixelType); - nativeSurface.SetCapability(new WebGPUSurfaceCapability( - (nint)gpuState.Device, - (nint)gpuState.Queue, - textureHandle, - textureViewHandle, - textureFormat, - width, - height, - isSrgb, - isPremultipliedAlpha)); - - surface = nativeSurface; - return true; - } - } - - internal void ReleaseNativeSurfaceTarget(nint textureHandle, nint textureViewHandle) - { - if ((textureHandle == 0 && textureViewHandle == 0) || this.isDisposed) - { - return; - } - - lock (this.gpuSync) - { - if (textureViewHandle != 0) - { - this.ReleaseTextureViewLocked((TextureView*)textureViewHandle); - } - - if (textureHandle != 0) - { - this.ReleaseTextureLocked((Texture*)textureHandle); - } - } - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 7c24c5f89..cd390cb72 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -2,9 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -17,255 +15,96 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -#pragma warning disable SA1201 // Elements should appear in the correct order /// /// WebGPU-backed implementation of . /// -/// -/// The public flow mirrors : -/// -/// FillPath enqueues normalized composition commands. -/// FlushCompositions executes the queued commands in order. -/// -/// GPU execution prepares coverage once (stencil-and-cover into R8 coverage), then composites all -/// queued commands against the active target session. If the pixel type is unsupported for GPU, -/// the whole flush delegates to . -/// internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; private const nuint CompositeInstanceBufferSize = 256 * 1024; private const int CallbackTimeoutMilliseconds = 10_000; - private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; - - private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; - - private readonly object gpuSync = new(); - private readonly ConcurrentDictionary coverageCache = new(); private readonly DefaultDrawingBackend fallbackBackend; - private WebGPURasterizer? coverageRasterizer; - private bool isDisposed; - private WebGPURuntime.Lease? runtimeLease; - private WebGPU? webGPU; - private Wgpu? wgpuExtension; - private Instance* instance; - private Adapter* adapter; - private Device* device; - private Queue* queue; - private BindGroupLayout* compositeBindGroupLayout; - private PipelineLayout* compositePipelineLayout; - private readonly ConcurrentDictionary compositePipelines = new(); - - private int compositeSessionDepth; - private bool compositeSessionGPUActive; - private bool compositeSessionDirty; - private readonly List compositeSessionCommands = []; - private RenderPassEncoder* compositeSessionPassEncoder; - private Rectangle compositeSessionTargetRectangle; - private Texture* compositeSessionTargetTexture; - private TextureView* compositeSessionTargetView; - private WgpuBuffer* compositeSessionReadbackBuffer; - private WgpuBuffer* compositeSessionInstanceBuffer; - private nuint compositeSessionInstanceBufferCapacity; - private CompositeInstanceData[]? compositeSessionInstanceScratch; - private CommandEncoder* compositeSessionCommandEncoder; - private uint compositeSessionReadbackBytesPerRow; - private ulong compositeSessionReadbackByteCount; - private int compositeSessionResourceWidth; - private int compositeSessionResourceHeight; - private TextureFormat compositeSessionResourceTextureFormat; - private bool compositeSessionRequiresReadback; - private bool compositeSessionOwnsTargetView; - private int liveCoverageCount; + private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); - private static readonly bool TraceEnabled = string.Equals( - Environment.GetEnvironmentVariable("IMAGESHARP_WEBGPU_TRACE"), - "1", - StringComparison.Ordinal); public WebGPUDrawingBackend() - { - this.fallbackBackend = DefaultDrawingBackend.Instance; - lock (this.gpuSync) - { - this.GPUInitializationAttempted = true; - this.LastGPUInitializationFailure = null; - this.IsGPUReady = this.TryInitializeGPULocked(); - } - } - - private static void Trace(string message) - { - if (TraceEnabled) - { - Console.Error.WriteLine($"[WebGPU] {message}"); - } - } - - /// - /// Gets the total number of coverage preparation requests. - /// - public int PrepareCoverageCallCount { get; private set; } + => this.fallbackBackend = DefaultDrawingBackend.Instance; /// - /// Gets the number of coverage preparations executed on the GPU. + /// Gets the testing-only diagnostic counter for total coverage preparation requests. /// - public int GPUPrepareCoverageCallCount { get; private set; } + internal int TestingPrepareCoverageCallCount { get; private set; } /// - /// Gets the number of coverage preparations delegated to the fallback backend. + /// Gets the testing-only diagnostic counter for coverage preparations executed on the GPU. /// - public int FallbackPrepareCoverageCallCount { get; private set; } + internal int TestingGPUPrepareCoverageCallCount { get; private set; } /// - /// Gets the total number of composition requests. + /// Gets the testing-only diagnostic counter for coverage preparations delegated to the fallback backend. /// - public int CompositeCoverageCallCount { get; private set; } + internal int TestingFallbackPrepareCoverageCallCount { get; private set; } /// - /// Gets the number of compositions executed on the GPU. + /// Gets the testing-only diagnostic counter for total composition requests. /// - public int GPUCompositeCoverageCallCount { get; private set; } + internal int TestingCompositeCoverageCallCount { get; private set; } /// - /// Gets the number of compositions delegated to the fallback backend. + /// Gets the testing-only diagnostic counter for compositions executed on the GPU. /// - public int FallbackCompositeCoverageCallCount { get; private set; } + internal int TestingGPUCompositeCoverageCallCount { get; private set; } /// - /// Gets the number of completed prepared-coverage uses. + /// Gets the testing-only diagnostic counter for compositions delegated to the fallback backend. /// - public int ReleaseCoverageCallCount { get; private set; } + internal int TestingFallbackCompositeCoverageCallCount { get; private set; } /// - /// Gets a value indicating whether the backend completed GPU initialization. + /// Gets the testing-only diagnostic counter for completed prepared-coverage uses. /// - public bool IsGPUReady { get; private set; } + internal int TestingReleaseCoverageCallCount { get; private set; } /// - /// Gets a value indicating whether GPU initialization has been attempted. + /// Gets a value indicating whether the testing-only diagnostic indicates the backend completed GPU initialization. /// - public bool GPUInitializationAttempted { get; private set; } + internal bool TestingIsGPUReady { get; private set; } /// - /// Gets the last GPU initialization failure reason, if any. + /// Gets a value indicating whether the testing-only diagnostic indicates GPU initialization has been attempted. /// - public string? LastGPUInitializationFailure { get; private set; } + internal bool TestingGPUInitializationAttempted { get; private set; } /// - /// Gets the number of live per-flush prepared coverage handles. + /// Gets the testing-only diagnostic containing the last GPU initialization failure reason, if any. /// - public int LiveCoverageCount => this.liveCoverageCount; + internal string? TestingLastGPUInitializationFailure { get; private set; } /// - /// Begins a composite session for a target region. + /// Gets the testing-only diagnostic counter for live prepared coverage handles currently in use. /// - /// - /// Nested calls are reference-counted. CPU targets are uploaded to a GPU session texture. - /// Native-surface targets bind directly to the surface view. - /// - public void BeginCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel - { - this.ThrowIfDisposed(); - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - - if (this.compositeSessionDepth > 0) - { - this.compositeSessionDepth++; - return; - } - - this.compositeSessionDepth = 1; - this.compositeSessionGPUActive = false; - this.compositeSessionDirty = false; - this.compositeSessionCommands.Clear(); - - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler) || - !this.IsGPUReady) - { - return; - } - - lock (this.gpuSync) - { - if (!this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _)) - { - return; - } - } - - this.ActivateCompositeSession(target, pixelHandler); - } + internal int TestingLiveCoverageCount { get; private set; } - /// - /// Ends a previously started composite session. - /// - /// - /// When this is the outermost session and GPU work has modified the active target, the - /// method either reads back into the CPU region (CPU session) or submits recorded commands - /// directly to the native surface (native session), then clears active session state. - /// - public void EndCompositeSession(Configuration configuration, ICanvasFrame target) - where TPixel : unmanaged, IPixel + internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) { this.ThrowIfDisposed(); - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(target, nameof(target)); - - if (this.compositeSessionDepth <= 0) - { - return; - } - - this.compositeSessionDepth--; - if (this.compositeSessionDepth > 0) - { - return; - } - - lock (this.gpuSync) + if (WebGPUFlushContext.TryGetInteropHandles(out deviceHandle, out queueHandle, out string? error)) { - Trace($"EndCompositeSession: gpuActive={this.compositeSessionGPUActive} dirty={this.compositeSessionDirty}"); - if (this.compositeSessionGPUActive && - this.compositeSessionDirty) - { - if (!this.TryDrainQueuedCompositeCommandsLocked()) - { - throw new InvalidOperationException("Failed to encode queued GPU composite commands."); - } - else if (this.compositeSessionRequiresReadback && - target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) - { - this.TryFlushCompositeSessionLocked(cpuTarget); - } - else if (!this.compositeSessionRequiresReadback) - { - this.TrySubmitCompositeSessionLocked(); - } - else - { - Trace("EndCompositeSession: skipped flush because CPU target was unavailable."); - } - } - - this.ResetCompositeSessionStateLocked(); + this.TestingGPUInitializationAttempted = true; + this.TestingIsGPUReady = true; + this.TestingLastGPUInitializationFailure = null; + return true; } - this.compositeSessionGPUActive = false; - this.compositeSessionDirty = false; + this.TestingGPUInitializationAttempted = true; + this.TestingIsGPUReady = false; + this.TestingLastGPUInitializationFailure = error; + return false; } - /// - /// Fills a path on the specified target region. - /// - /// - /// The method clips interest bounds to the local target region, prepares reusable coverage, - /// then composites that coverage with the supplied brush. - /// + /// public void FillPath( Configuration configuration, ICanvasFrame target, @@ -299,68 +138,113 @@ public void FlushCompositions( return; } - if (!this.TryBeginGPUFlush(target, out bool openedCompositeSession)) - { - this.FlushCompositionsFallback(configuration, target, compositionBatch); - return; - } + int commandCount = compositionBatch.Commands.Count; + this.TestingPrepareCoverageCallCount++; + this.TestingReleaseCoverageCallCount++; + this.TestingCompositeCoverageCallCount += commandCount; - CompositionCoverageDefinition definition = compositionBatch.Definition; - RasterizerOptions rasterizerOptions = definition.RasterizerOptions; - CoverageEntry? coverageEntry = this.PrepareCoverageEntry( - definition.Path, - in rasterizerOptions); - if (coverageEntry is null) + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) { - if (openedCompositeSession) - { - this.EndCompositeSession(configuration, target); - } - + this.TestingFallbackPrepareCoverageCallCount++; + this.TestingFallbackCompositeCoverageCallCount += commandCount; this.FlushCompositionsFallback(configuration, target, compositionBatch); return; } - this.liveCoverageCount++; - this.ReleaseCoverageCallCount++; + bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); + bool hasNativeSurface = target.TryGetNativeSurface(out _); + bool useCpuReadbackFlushSession = hasCpuRegion && !hasNativeSurface && compositionBatch.FlushId != 0; + bool gpuSuccess = false; + bool gpuReady = false; + string? failure = null; + bool hadExistingCpuSession = false; + WebGPUFlushContext? flushContext = null; + try { - IReadOnlyList commands = compositionBatch.Commands; - Rectangle targetBounds = target.Bounds; - lock (this.gpuSync) + flushContext = useCpuReadbackFlushSession + ? WebGPUFlushContext.GetOrCreateCpuReadbackFlushContext( + compositionBatch.FlushId, + target, + pixelHandler.TextureFormat, + pixelHandler.PixelSizeInBytes, + out hadExistingCpuSession) + : WebGPUFlushContext.Create(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); + + lock (flushContext.DeviceState.SyncRoot) { - this.compositeSessionCommands.EnsureCapacity(this.compositeSessionCommands.Count + commands.Count); - for (int i = 0; i < commands.Count; i++) + CompositionCoverageDefinition definition = compositionBatch.Definition; + if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline(flushContext.TextureFormat, out RenderPipeline* pipeline, out failure)) { - PreparedCompositionCommand command = commands[i]; - this.CompositeCoverageCallCount++; - - if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) + gpuSuccess = false; + } + else if (!flushContext.DeviceState.TryGetOrCreateCoverageEntry( + in definition, + flushContext.Queue, + out WebGPUFlushContext.CoverageEntry? coverageEntry, + out failure) || coverageEntry is null) + { + gpuSuccess = false; + } + else + { + gpuReady = true; + gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); + if (gpuSuccess && (!useCpuReadbackFlushSession || compositionBatch.IsFinalBatchInFlush)) { - throw new InvalidOperationException("Unsupported brush for WebGPU composition."); + gpuSuccess = this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion); } - - this.QueueCompositeCoverageLocked( - coverageEntry, - targetBounds, - command.DestinationRegion, - command.SourceOffset, - brushData, - command.GraphicsOptions.BlendPercentage); - - this.GPUCompositeCoverageCallCount++; } } } - finally + catch (Exception ex) + { + failure = ex.Message; + gpuSuccess = false; + } + + this.TestingGPUInitializationAttempted = true; + this.TestingIsGPUReady = gpuReady; + this.TestingLastGPUInitializationFailure = gpuSuccess ? null : failure; + this.TestingLiveCoverageCount = 0; + + if (useCpuReadbackFlushSession) { - this.liveCoverageCount--; + if (gpuSuccess) + { + this.TestingGPUPrepareCoverageCallCount++; + this.TestingGPUCompositeCoverageCallCount += commandCount; + if (compositionBatch.IsFinalBatchInFlush) + { + WebGPUFlushContext.CompleteCpuReadbackFlushContext(compositionBatch.FlushId); + } + + return; + } - if (openedCompositeSession) + WebGPUFlushContext.CompleteCpuReadbackFlushContext(compositionBatch.FlushId); + if (hadExistingCpuSession) { - this.EndCompositeSession(configuration, target); + throw new InvalidOperationException($"WebGPU CPURegion flush session failed after prior GPU batches. Reason: {failure ?? "Unknown error"}"); } + + this.TestingFallbackPrepareCoverageCallCount++; + this.TestingFallbackCompositeCoverageCallCount += commandCount; + this.FlushCompositionsFallback(configuration, target, compositionBatch); + return; + } + + flushContext?.Dispose(); + if (gpuSuccess) + { + this.TestingGPUPrepareCoverageCallCount++; + this.TestingGPUCompositeCoverageCallCount += commandCount; + return; } + + this.TestingFallbackPrepareCoverageCallCount++; + this.TestingFallbackCompositeCoverageCallCount += commandCount; + this.FlushCompositionsFallback(configuration, target, compositionBatch); } private void FlushCompositionsFallback( @@ -369,1634 +253,418 @@ private void FlushCompositionsFallback( CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel { - this.PrepareCoverageCallCount++; - this.FallbackPrepareCoverageCallCount++; - this.ReleaseCoverageCallCount++; - this.CompositeCoverageCallCount += compositionBatch.Commands.Count; - this.FallbackCompositeCoverageCallCount += compositionBatch.Commands.Count; - if (target.TryGetCpuRegion(out _)) { this.fallbackBackend.FlushCompositions(configuration, target, compositionBatch); return; } - if (!TryGetNativeSurfaceCapability( - target, - expectedTargetFormat: null, - requireWritableTexture: true, - out WebGPUSurfaceCapability? surfaceCapability)) - { - throw new NotSupportedException( - "Fallback composition requires either a CPU destination region or a native WebGPU surface exposing a writable texture handle."); - } - Rectangle targetBounds = target.Bounds; - using Buffer2D stagingBuffer = configuration.MemoryAllocator.Allocate2D( - new Size(targetBounds.Width, targetBounds.Height), - AllocationOptions.Clean); - Buffer2DRegion stagingRegion = new(stagingBuffer, targetBounds); + using WebGPUFlushContext.FallbackStagingLease stagingLease = + WebGPUFlushContext.RentFallbackStaging(configuration.MemoryAllocator, in targetBounds); + + Buffer2DRegion stagingRegion = stagingLease.Region; ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionBatch); - lock (this.gpuSync) + using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target); + lock (uploadContext.DeviceState.SyncRoot) { - if (!this.QueueWriteTextureFromRegionLocked((Texture*)surfaceCapability.TargetTexture, stagingRegion)) + if (!this.QueueWriteTextureFromRegion( + uploadContext.Api, + uploadContext.Queue, + uploadContext.TargetTexture, + stagingRegion)) { - throw new NotSupportedException( - "Fallback composition could not upload to the native WebGPU target texture."); + throw new NotSupportedException("Fallback upload to native WebGPU target failed."); } } } - private bool TryBeginGPUFlush(ICanvasFrame target, out bool openedCompositeSession) - where TPixel : unmanaged, IPixel + private bool TryCompositeBatch( + WebGPUFlushContext flushContext, + RenderPipeline* pipeline, + WebGPUFlushContext.CoverageEntry coverageEntry, + in Rectangle destinationBounds, + IReadOnlyList commands) { - openedCompositeSession = false; - if (this.compositeSessionDepth > 0) + int commandCount = commands.Count; + if (commandCount == 0) { - return this.compositeSessionGPUActive; + return true; } - if (!this.IsGPUReady || - !CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) + nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); + if (!flushContext.EnsureInstanceBufferCapacity(instanceBytes, CompositeInstanceBufferSize) || + !flushContext.EnsureCommandEncoder() || + !flushContext.BeginRenderPass()) { return false; } - lock (this.gpuSync) + CompositeInstanceData[] rented = ArrayPool.Shared.Rent(commandCount); + try { - if (!this.TryGetOrCreateCompositePipelineLocked(pixelHandler.TextureFormat, out _)) + Span instances = rented.AsSpan(0, commandCount); + int targetWidth = flushContext.TargetBounds.Width; + int targetHeight = flushContext.TargetBounds.Height; + for (int i = 0; i < commandCount; i++) + { + PreparedCompositionCommand command = commands[i]; + if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) + { + return false; + } + + int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; + int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; + + instances[i] = new CompositeInstanceData + { + SourceOffsetX = (uint)command.SourceOffset.X, + SourceOffsetY = (uint)command.SourceOffset.Y, + DestinationX = (uint)destinationX, + DestinationY = (uint)destinationY, + DestinationWidth = (uint)command.DestinationRegion.Width, + DestinationHeight = (uint)command.DestinationRegion.Height, + TargetWidth = (uint)targetWidth, + TargetHeight = (uint)targetHeight, + BrushKind = (uint)brushData.Kind, + SolidBrushColor = brushData.SolidColor, + BlendPercentage = command.GraphicsOptions.BlendPercentage + }; + } + + fixed (CompositeInstanceData* instancesPtr = instances) + { + flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, 0, instancesPtr, instanceBytes); + } + + BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceBytes); + if (bindGroup is null) { return false; } - } - this.compositeSessionDepth = 1; - this.compositeSessionGPUActive = false; - this.compositeSessionDirty = false; - this.compositeSessionCommands.Clear(); - if (!this.ActivateCompositeSession(target, pixelHandler)) + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); + flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, (uint)commandCount, 0, 0); + + return true; + } + finally { - this.compositeSessionDepth = 0; - this.compositeSessionGPUActive = false; - this.compositeSessionDirty = false; - this.compositeSessionCommands.Clear(); - return false; + ArrayPool.Shared.Return(rented); } - - openedCompositeSession = true; - return true; } - private CoverageEntry? PrepareCoverageEntry( - IPath path, - in RasterizerOptions rasterizerOptions) + private BindGroup* CreateCoverageBindGroup( + WebGPUFlushContext flushContext, + WebGPUFlushContext.CoverageEntry coverageEntry, + nuint instanceBytes) { - this.ThrowIfDisposed(); - Guard.NotNull(path, nameof(path)); - - this.PrepareCoverageCallCount++; - int definitionKey = CompositionCommand.ComputeCoverageDefinitionKey(path, in rasterizerOptions); - CoverageEntry? entry = this.GetOrCreateCoverageEntry(definitionKey, path, in rasterizerOptions); - if (entry is null) + if (flushContext.DeviceState.CompositeBindGroupLayout is null || + coverageEntry.GPUCoverageView is null || + flushContext.InstanceBuffer is null) { - this.FallbackPrepareCoverageCallCount++; return null; } - this.GPUPrepareCoverageCallCount++; - return entry; + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageEntry.GPUCoverageView + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = flushContext.InstanceBuffer, + Offset = 0, + Size = instanceBytes + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = flushContext.DeviceState.CompositeBindGroupLayout, + EntryCount = 2, + Entries = bindGroupEntries + }; + + return flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); } - private CoverageEntry? GetOrCreateCoverageEntry( - int definitionKey, - IPath path, - in RasterizerOptions rasterizerOptions) + private bool TryFinalizeFlush( + WebGPUFlushContext flushContext, + bool hasCpuRegion, + Buffer2DRegion cpuRegion) + where TPixel : unmanaged, IPixel { - if (this.coverageCache.TryGetValue(definitionKey, out CoverageEntry? cached)) - { - return cached; - } + flushContext.EndRenderPassIfOpen(); + return flushContext.RequiresReadback + ? hasCpuRegion && this.TryReadBackToCpuRegion(flushContext, cpuRegion) + : TrySubmit(flushContext); + } - CoverageEntry? created = this.CreateCoverageEntry(path, in rasterizerOptions); - if (created is null) + private static bool TrySubmit(WebGPUFlushContext flushContext) + { + CommandEncoder* commandEncoder = flushContext.CommandEncoder; + if (commandEncoder is null) { - return null; + return true; } - CoverageEntry winner = this.coverageCache.GetOrAdd(definitionKey, created); - if (!ReferenceEquals(winner, created)) + CommandBuffer* commandBuffer = null; + try { - lock (this.gpuSync) + CommandBufferDescriptor descriptor = default; + commandBuffer = flushContext.Api.CommandEncoderFinish(commandEncoder, in descriptor); + if (commandBuffer is null) { - this.ReleaseCoverageTextureLocked(created); + return false; } - created.Dispose(); + flushContext.Api.QueueSubmit(flushContext.Queue, 1, ref commandBuffer); + flushContext.Api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + flushContext.Api.CommandEncoderRelease(commandEncoder); + flushContext.CommandEncoder = null; + return true; + } + finally + { + if (commandBuffer is not null) + { + flushContext.Api.CommandBufferRelease(commandBuffer); + } } - - return winner; } - private CoverageEntry? CreateCoverageEntry(IPath path, in RasterizerOptions rasterizerOptions) + private bool TryReadBackToCpuRegion(WebGPUFlushContext flushContext, Buffer2DRegion destinationRegion) + where TPixel : unmanaged, IPixel { - Texture* coverageTexture = null; - TextureView* coverageView = null; - lock (this.gpuSync) + if (flushContext.TargetTexture is null || + flushContext.ReadbackBuffer is null || + flushContext.ReadbackByteCount == 0 || + flushContext.ReadbackBytesPerRow == 0) { - WebGPURasterizer? rasterizer = this.coverageRasterizer; - if (rasterizer is null || - !rasterizer.TryCreateCoverageTexture(path, in rasterizerOptions, out coverageTexture, out coverageView)) - { - return null; - } + return false; + } + + if (!flushContext.EnsureCommandEncoder()) + { + return false; } - Size size = rasterizerOptions.Interest.Size; - return new CoverageEntry(size.Width, size.Height) + ImageCopyTexture source = new() { - GPUCoverageTexture = coverageTexture, - GPUCoverageView = coverageView + Texture = flushContext.TargetTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All }; - } - /// - /// Releases all cached coverage and GPU resources owned by this backend instance. - /// - public void Dispose() - { - if (this.isDisposed) + ImageCopyBuffer destination = new() { - return; - } + Buffer = flushContext.ReadbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = flushContext.ReadbackBytesPerRow, + RowsPerImage = (uint)destinationRegion.Height + } + }; + + Extent3D copySize = new((uint)destinationRegion.Width, (uint)destinationRegion.Height, 1); + flushContext.Api.CommandEncoderCopyTextureToBuffer(flushContext.CommandEncoder, in source, in destination, in copySize); - Trace("Dispose: begin"); - lock (this.gpuSync) + if (!TrySubmit(flushContext)) { - this.ResetCompositeSessionStateLocked(); - this.ReleaseCompositeSessionResourcesLocked(); - this.ReleaseGPUResourcesLocked(); + return false; } - this.isDisposed = true; - Trace("Dispose: end"); + return this.TryReadBackBufferToRegion( + flushContext, + flushContext.ReadbackBuffer, + checked((int)flushContext.ReadbackBytesPerRow), + destinationRegion); } - private bool ActivateCompositeSession( - ICanvasFrame target, - in CompositePixelRegistration pixelHandler) - where TPixel : unmanaged, IPixel + private bool TryReadBackBufferToRegion( + WebGPUFlushContext flushContext, + WgpuBuffer* readbackBuffer, + int sourceRowBytes, + Buffer2DRegion destinationRegion) + where TPixel : unmanaged { - lock (this.gpuSync) + int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); + int readbackByteCount = checked(sourceRowBytes * destinationRegion.Height); + if (!this.TryMapReadBuffer(flushContext, readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) { - bool started = false; - if (target.TryGetCpuRegion(out Buffer2DRegion cpuTarget)) - { - started = this.BeginCompositeSessionCoreLocked( - cpuTarget, - pixelHandler.TextureFormat, - pixelHandler.PixelSizeInBytes); - } - else if (TryGetNativeSurfaceCapability( - target, - expectedTargetFormat: pixelHandler.TextureFormat, - requireWritableTexture: false, - out WebGPUSurfaceCapability? nativeSurfaceCapability) && - this.BeginCompositeSurfaceSessionCoreLocked(target, nativeSurfaceCapability)) + return false; + } + + try + { + ReadOnlySpan sourceData = new(mappedData, readbackByteCount); + int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); + + if (destinationRegion.Rectangle.X == 0 && + sourceRowBytes == destinationStrideBytes && + TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) { - started = true; + Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); + int destinationStart = checked(destinationRegion.Rectangle.Y * destinationStrideBytes); + int copyByteCount = checked(destinationStrideBytes * destinationRegion.Height); + sourceData[..copyByteCount].CopyTo(destinationBytes.Slice(destinationStart, copyByteCount)); + return true; } - if (!started) + for (int y = 0; y < destinationRegion.Height; y++) { - return false; + ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); + MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); } - this.compositeSessionGPUActive = true; return true; } + finally + { + flushContext.Api.BufferUnmap(readbackBuffer); + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetNativeSurfaceCapability( - ICanvasFrame target, - TextureFormat? expectedTargetFormat, - bool requireWritableTexture, - [NotNullWhen(true)] out WebGPUSurfaceCapability? capability) - where TPixel : unmanaged, IPixel + private bool TryMapReadBuffer( + WebGPUFlushContext flushContext, + WgpuBuffer* readbackBuffer, + nuint byteCount, + out byte* mappedData) { - if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || - !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? surfaceCapability)) + mappedData = null; + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim callbackReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userData) { - capability = null; - return false; + mapStatus = status; + callbackReady.Set(); } - if (expectedTargetFormat is TextureFormat requiredFormat) + using PfnBufferMapCallback callbackPtr = PfnBufferMapCallback.From(Callback); + flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); + + if (!WaitForSignal(flushContext, callbackReady) || mapStatus != BufferMapAsyncStatus.Success) { - if (surfaceCapability.TargetTextureView == 0 || - surfaceCapability.TargetFormat != requiredFormat) - { - capability = null; - return false; - } + return false; } - if (requireWritableTexture && surfaceCapability.TargetTexture == 0) + void* mapped = flushContext.Api.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); + if (mapped is null) { - capability = null; + flushContext.Api.BufferUnmap(readbackBuffer); return false; } - capability = surfaceCapability; + mappedData = (byte*)mapped; return true; } - /// - /// Performs one-time GPU initialization while is held. - /// - private bool TryInitializeGPULocked() + private bool QueueWriteTextureFromRegion( + WebGPU api, + Queue* queue, + Texture* destinationTexture, + Buffer2DRegion sourceRegion) + where TPixel : unmanaged { - Trace("TryInitializeGPULocked: begin"); - try - { - this.runtimeLease = WebGPURuntime.Acquire(); - this.webGPU = this.runtimeLease.Api; - this.wgpuExtension = this.runtimeLease.WgpuExtension; - Trace($"TryInitializeGPULocked: extension={(this.wgpuExtension is null ? "none" : "wgpu.h")}"); - this.instance = this.webGPU.CreateInstance((InstanceDescriptor*)null); - if (this.instance is null) - { - this.LastGPUInitializationFailure = "WebGPU.CreateInstance returned null."; - Trace("TryInitializeGPULocked: CreateInstance returned null"); - return false; - } - - Trace("TryInitializeGPULocked: created instance"); - if (!this.TryRequestAdapterLocked(out this.adapter) || this.adapter is null) - { - this.LastGPUInitializationFailure ??= "Failed to request WebGPU adapter."; - Trace($"TryInitializeGPULocked: request adapter failed ({this.LastGPUInitializationFailure})"); - return false; - } - - Trace("TryInitializeGPULocked: adapter acquired"); - if (!this.TryRequestDeviceLocked(out this.device) || this.device is null) - { - this.LastGPUInitializationFailure ??= "Failed to request WebGPU device."; - Trace($"TryInitializeGPULocked: request device failed ({this.LastGPUInitializationFailure})"); - return false; - } - - this.queue = this.webGPU.DeviceGetQueue(this.device); - if (this.queue is null) - { - this.LastGPUInitializationFailure = "WebGPU.DeviceGetQueue returned null."; - Trace("TryInitializeGPULocked: DeviceGetQueue returned null"); - return false; - } - - Trace("TryInitializeGPULocked: queue acquired"); - if (!this.TryCreateCompositePipelineLocked()) - { - this.LastGPUInitializationFailure = "Failed to create WebGPU composite pipeline."; - Trace("TryInitializeGPULocked: composite pipeline creation failed"); - return false; - } - - Trace("TryInitializeGPULocked: composite pipeline ready"); - this.coverageRasterizer = new WebGPURasterizer(this.webGPU, this.device, this.queue); - if (!this.coverageRasterizer.Initialize()) - { - this.LastGPUInitializationFailure = "Failed to create WebGPU coverage pipeline."; - Trace("TryInitializeGPULocked: coverage pipeline creation failed"); - return false; - } - - Trace("TryInitializeGPULocked: coverage pipeline ready"); - return true; - } - catch (Exception ex) - { - this.LastGPUInitializationFailure = $"WebGPU initialization threw: {ex.Message}"; - Trace($"TryInitializeGPULocked: exception {ex}"); - return false; - } - finally - { - if (!this.IsGPUReady && - (this.compositePipelineLayout is null || - this.compositeBindGroupLayout is null || - this.coverageRasterizer is null || - !this.coverageRasterizer.IsInitialized || - this.device is null || - this.queue is null)) - { - this.LastGPUInitializationFailure ??= "WebGPU initialization left required resources unavailable."; - this.ReleaseGPUResourcesLocked(); - } - - Trace($"TryInitializeGPULocked: end ready={this.IsGPUReady} error={this.LastGPUInitializationFailure ?? ""}"); - } - } - - private bool TryRequestAdapterLocked(out Adapter* resultAdapter) - { - resultAdapter = null; - if (this.webGPU is null || this.instance is null) - { - return false; - } - - RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; - Adapter* callbackAdapter = null; - using ManualResetEventSlim callbackReady = new(false); - void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* messagePtr, void* userDataPtr) - { - callbackStatus = status; - callbackAdapter = adapterPtr; - _ = messagePtr; - _ = userDataPtr; - callbackReady.Set(); - } - - using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); - RequestAdapterOptions options = new() - { - PowerPreference = PowerPreference.HighPerformance - }; - - this.webGPU.InstanceRequestAdapter(this.instance, in options, callbackPtr, null); - if (!this.WaitForSignalLocked(callbackReady)) - { - this.LastGPUInitializationFailure = "Timed out while waiting for WebGPU adapter request callback."; - Trace("TryRequestAdapterLocked: timeout waiting for callback"); - return false; - } - - resultAdapter = callbackAdapter; - if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) - { - this.LastGPUInitializationFailure = $"WebGPU adapter request failed with status '{callbackStatus}'."; - Trace($"TryRequestAdapterLocked: callback status={callbackStatus} adapter={(nint)callbackAdapter:X}"); - return false; - } - - return true; - } - - private bool TryRequestDeviceLocked(out Device* resultDevice) - { - resultDevice = null; - if (this.webGPU is null || this.adapter is null) - { - return false; - } - - RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; - Device* callbackDevice = null; - using ManualResetEventSlim callbackReady = new(false); - void Callback(RequestDeviceStatus status, Device* devicePtr, byte* messagePtr, void* userDataPtr) - { - callbackStatus = status; - callbackDevice = devicePtr; - _ = messagePtr; - _ = userDataPtr; - callbackReady.Set(); - } - - using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); - DeviceDescriptor descriptor = default; - this.webGPU.AdapterRequestDevice(this.adapter, in descriptor, callbackPtr, null); - - if (!this.WaitForSignalLocked(callbackReady)) - { - this.LastGPUInitializationFailure = "Timed out while waiting for WebGPU device request callback."; - Trace("TryRequestDeviceLocked: timeout waiting for callback"); - return false; - } - - resultDevice = callbackDevice; - if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) - { - this.LastGPUInitializationFailure = $"WebGPU device request failed with status '{callbackStatus}'."; - Trace($"TryRequestDeviceLocked: callback status={callbackStatus} device={(nint)callbackDevice:X}"); - return false; - } - - return true; - } - - /// - /// Creates the render pipeline used for coverage composition. - /// - private bool TryCreateCompositePipelineLocked() - { - if (this.webGPU is null || this.device is null) - { - return false; - } - - BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; - layoutEntries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Fragment, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - layoutEntries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Vertex | ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = (ulong)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor layoutDescriptor = new() - { - EntryCount = 2, - Entries = layoutEntries - }; - - this.compositeBindGroupLayout = this.webGPU.DeviceCreateBindGroupLayout(this.device, in layoutDescriptor); - if (this.compositeBindGroupLayout is null) - { - return false; - } - - BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; - bindGroupLayouts[0] = this.compositeBindGroupLayout; - PipelineLayoutDescriptor pipelineLayoutDescriptor = new() - { - BindGroupLayoutCount = 1, - BindGroupLayouts = bindGroupLayouts - }; - - this.compositePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); - if (this.compositePipelineLayout is null) - { - return false; - } - - // Validate that at least the baseline RGBA target format can create a pipeline. - if (!this.TryGetOrCreateCompositePipelineLocked(TextureFormat.Rgba8Unorm, out _)) - { - return false; - } - - // BGRA is optional and can fail on specific adapters/drivers. - _ = this.TryGetOrCreateCompositePipelineLocked(TextureFormat.Bgra8Unorm, out _); - return true; - } - - private bool TryGetOrCreateCompositePipelineLocked(TextureFormat textureFormat, out RenderPipeline* pipeline) - { - pipeline = null; - if (textureFormat == TextureFormat.Undefined || - this.webGPU is null || - this.device is null || - this.compositePipelineLayout is null) - { - return false; - } - - if (this.compositePipelines.TryGetValue(textureFormat, out nint existingPipelineHandle) && - existingPipelineHandle != 0) - { - pipeline = (RenderPipeline*)existingPipelineHandle; - return true; - } - - RenderPipeline* createdPipeline = this.CreateCompositePipelineForFormatLocked(textureFormat); - if (createdPipeline is null) - { - return false; - } - - nint createdPipelineHandle = (nint)createdPipeline; - nint cachedPipelineHandle = this.compositePipelines.GetOrAdd(textureFormat, createdPipelineHandle); - if (cachedPipelineHandle != createdPipelineHandle) - { - this.webGPU.RenderPipelineRelease(createdPipeline); - } - - pipeline = (RenderPipeline*)cachedPipelineHandle; - return pipeline is not null; - } - - private RenderPipeline* CreateCompositePipelineForFormatLocked(TextureFormat textureFormat) - { - if (this.webGPU is null || this.device is null) - { - return null; - } - - ShaderModule* shaderModule = null; - try - { - ReadOnlySpan shaderCode = CompositeCoverageShader.Code; - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct - { - SType = SType.ShaderModuleWgslDescriptor - }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.webGPU.DeviceCreateShaderModule(this.device, in shaderDescriptor); - } - - if (shaderModule is null) - { - return null; - } - - ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; - ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; - fixed (byte* vertexEntryPointPtr = vertexEntryPoint) - { - fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) - { - return this.CreateCompositePipelineLocked( - shaderModule, - vertexEntryPointPtr, - fragmentEntryPointPtr, - textureFormat); - } - } - } - finally - { - if (shaderModule is not null) - { - this.webGPU.ShaderModuleRelease(shaderModule); - } - } - } - - private RenderPipeline* CreateCompositePipelineLocked( - ShaderModule* shaderModule, - byte* vertexEntryPointPtr, - byte* fragmentEntryPointPtr, - TextureFormat textureFormat) - { - if (this.webGPU is null || this.device is null || this.compositePipelineLayout is null) - { - return null; - } - - VertexState vertexState = new() - { - Module = shaderModule, - EntryPoint = vertexEntryPointPtr, - BufferCount = 0, - Buffers = null - }; - - BlendState blendState = new() - { - Color = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - }, - Alpha = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - } - }; - - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; - colorTargets[0] = new ColorTargetState - { - Format = textureFormat, - Blend = &blendState, - WriteMask = ColorWriteMask.All - }; - - FragmentState fragmentState = new() - { - Module = shaderModule, - EntryPoint = fragmentEntryPointPtr, - TargetCount = 1, - Targets = colorTargets - }; - - RenderPipelineDescriptor pipelineDescriptor = new() - { - Layout = this.compositePipelineLayout, - Vertex = vertexState, - Primitive = new PrimitiveState - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }, - DepthStencil = null, - Multisample = new MultisampleState - { - Count = 1, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }, - Fragment = &fragmentState - }; - - return this.webGPU.DeviceCreateRenderPipeline(this.device, in pipelineDescriptor); - } - - private bool WaitForSignalLocked(ManualResetEventSlim signal) - { - Stopwatch timer = Stopwatch.StartNew(); - SpinWait spinner = default; - while (!signal.IsSet) - { - if (timer.ElapsedMilliseconds >= CallbackTimeoutMilliseconds) - { - return false; - } - - if (this.wgpuExtension is not null && this.device is not null) - { - _ = this.wgpuExtension.DevicePoll(this.device, false, (WrappedSubmissionIndex*)null); - continue; - } - - if (this.instance is not null && this.webGPU is not null) - { - this.webGPU.InstanceProcessEvents(this.instance); - } - - if (!signal.IsSet) - { - if (spinner.Count < 10) - { - spinner.SpinOnce(); - } - else - { - Thread.Yield(); - } - } - } - - return true; - } - - private bool QueueWriteTextureFromRegionLocked(Texture* destinationTexture, Buffer2DRegion sourceRegion) - where TPixel : unmanaged - { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - WebGPU api = gpuState.Api; - Queue* queue = gpuState.Queue; - int pixelSizeInBytes = Unsafe.SizeOf(); - ImageCopyTexture destination = new() - { - Texture = destinationTexture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All - }; - - Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); - - // For full-row regions in a contiguous buffer, upload directly with source stride. - // For subregions, prefer tightly packed upload to avoid transferring row gaps. - if (IsSingleMemory(sourceRegion.Buffer) && - sourceRegion.Rectangle.X == 0 && - sourceRegion.Width == sourceRegion.Buffer.Width) - { - int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); - int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)sourceStrideBytes, - RowsPerImage = (uint)sourceRegion.Height - }; - - Span firstRow = sourceRegion.DangerousGetRowSpan(0); - fixed (TPixel* uploadPtr = firstRow) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); - } - - return true; - } - - int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - int packedByteCount = checked(packedRowBytes * sourceRegion.Height); - byte[] rented = ArrayPool.Shared.Rent(packedByteCount); - try - { - Span packedData = rented.AsSpan(0, packedByteCount); - for (int y = 0; y < sourceRegion.Height; y++) - { - ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); - MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); - } - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)packedRowBytes, - RowsPerImage = (uint)sourceRegion.Height - }; - - fixed (byte* uploadPtr = packedData) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); - } - - return true; - } - catch - { - return false; - } - finally - { - ArrayPool.Shared.Return(rented); - } - } - - /// - /// Ensures session resources for the target size, then uploads target pixels once. - /// - private bool BeginCompositeSessionCoreLocked( - Buffer2DRegion target, - TextureFormat textureFormat, - int pixelSizeInBytes) - where TPixel : unmanaged - { - if (!this.EnsureCompositeSessionResourcesLocked(target.Width, target.Height, textureFormat, pixelSizeInBytes) || - this.compositeSessionTargetTexture is null) - { - return false; - } - - this.ResetCompositeSessionStateLocked(); - if (!this.QueueWriteTextureFromRegionLocked(this.compositeSessionTargetTexture, target)) - { - return false; - } - - this.compositeSessionTargetRectangle = target.Rectangle; - this.compositeSessionRequiresReadback = true; - this.compositeSessionOwnsTargetView = true; - this.compositeSessionDirty = false; - return true; - } - - private bool BeginCompositeSurfaceSessionCoreLocked( - ICanvasFrame target, - WebGPUSurfaceCapability nativeSurfaceCapability) - where TPixel : unmanaged, IPixel - { - if (nativeSurfaceCapability.TargetTextureView == 0 || - target.Bounds.Width <= 0 || - target.Bounds.Height <= 0) - { - return false; - } - - if (target.Bounds.Right > nativeSurfaceCapability.Width || - target.Bounds.Bottom > nativeSurfaceCapability.Height) - { - return false; - } - - if (!this.TryGetOrCreateCompositePipelineLocked(nativeSurfaceCapability.TargetFormat, out _)) - { - return false; - } - - this.ResetCompositeSessionStateLocked(); - if (this.compositeSessionOwnsTargetView) - { - this.ReleaseTextureViewLocked(this.compositeSessionTargetView); - } - - this.ReleaseTextureLocked(this.compositeSessionTargetTexture); - this.compositeSessionTargetTexture = null; - this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); - this.compositeSessionReadbackBuffer = null; - this.compositeSessionReadbackBytesPerRow = 0; - this.compositeSessionReadbackByteCount = 0; - this.compositeSessionResourceWidth = 0; - this.compositeSessionResourceHeight = 0; - this.compositeSessionResourceTextureFormat = nativeSurfaceCapability.TargetFormat; - this.compositeSessionTargetView = (TextureView*)nativeSurfaceCapability.TargetTextureView; - this.compositeSessionOwnsTargetView = false; - this.compositeSessionRequiresReadback = false; - this.compositeSessionTargetRectangle = target.Bounds; - this.compositeSessionDirty = false; - return true; - } - - private bool EnsureCompositeSessionResourcesLocked( - int width, - int height, - TextureFormat textureFormat, - int pixelSizeInBytes) - { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - if (this.compositeSessionTargetTexture is not null && - this.compositeSessionTargetView is not null && - this.compositeSessionReadbackBuffer is not null && - this.compositeSessionResourceWidth == width && - this.compositeSessionResourceHeight == height && - this.compositeSessionResourceTextureFormat == textureFormat) - { - return this.TryEnsureCompositeSessionInstanceBufferCapacityLocked( - in gpuState, - (nuint)Unsafe.SizeOf()); - } - - this.ReleaseCompositeSessionResourcesLocked(); - - uint textureRowBytes = checked((uint)width * (uint)pixelSizeInBytes); - uint readbackRowBytes = AlignTo256(textureRowBytes); - ulong readbackByteCount = (ulong)readbackRowBytes * (uint)height; - - TextureDescriptor targetTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = textureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* targetTexture = gpuState.Api.DeviceCreateTexture(gpuState.Device, in targetTextureDescriptor); - if (targetTexture is null) - { - return false; - } - - TextureViewDescriptor targetViewDescriptor = new() - { - Format = textureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* targetView = gpuState.Api.TextureCreateView(targetTexture, in targetViewDescriptor); - if (targetView is null) - { - this.ReleaseTextureLocked(targetTexture); - return false; - } - - BufferDescriptor readbackBufferDescriptor = new() - { - Usage = BufferUsage.MapRead | BufferUsage.CopyDst, - Size = readbackByteCount - }; - - WgpuBuffer* readbackBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in readbackBufferDescriptor); - if (readbackBuffer is null) - { - this.ReleaseTextureViewLocked(targetView); - this.ReleaseTextureLocked(targetTexture); - return false; - } - - this.compositeSessionTargetTexture = targetTexture; - this.compositeSessionTargetView = targetView; - this.compositeSessionReadbackBuffer = readbackBuffer; - this.compositeSessionReadbackBytesPerRow = readbackRowBytes; - this.compositeSessionReadbackByteCount = readbackByteCount; - this.compositeSessionResourceWidth = width; - this.compositeSessionResourceHeight = height; - this.compositeSessionResourceTextureFormat = textureFormat; - this.compositeSessionRequiresReadback = true; - this.compositeSessionOwnsTargetView = true; - return this.TryEnsureCompositeSessionInstanceBufferCapacityLocked( - in gpuState, - (nuint)Unsafe.SizeOf()); - } - - private bool TryEnsureCompositeSessionInstanceBufferCapacityLocked(in GPUState gpuState, nuint requiredBytes) - { - if (requiredBytes == 0) - { - return true; - } - - if (this.compositeSessionInstanceBuffer is not null && - this.compositeSessionInstanceBufferCapacity >= requiredBytes) - { - return true; - } - - this.ReleaseAllCoverageCompositeBindGroupsLocked(); - this.ReleaseBufferLocked(this.compositeSessionInstanceBuffer); - - nuint targetSize = requiredBytes > CompositeInstanceBufferSize - ? requiredBytes - : CompositeInstanceBufferSize; - - BufferDescriptor instanceBufferDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = targetSize - }; - - this.compositeSessionInstanceBuffer = gpuState.Api.DeviceCreateBuffer(gpuState.Device, in instanceBufferDescriptor); - if (this.compositeSessionInstanceBuffer is null) - { - this.compositeSessionInstanceBufferCapacity = 0; - return false; - } - - this.compositeSessionInstanceBufferCapacity = targetSize; - return true; - } - - /// - /// Reads the session target texture back into the canvas region. - /// - private bool TryFlushCompositeSessionLocked(Buffer2DRegion target) - where TPixel : unmanaged, IPixel - { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - Trace("TryFlushCompositeSessionLocked: begin"); - int targetWidth = this.compositeSessionTargetRectangle.Width; - int targetHeight = this.compositeSessionTargetRectangle.Height; - if (this.compositeSessionTargetTexture is null || - this.compositeSessionReadbackBuffer is null || - targetWidth <= 0 || - targetHeight <= 0 || - this.compositeSessionReadbackByteCount == 0 || - this.compositeSessionReadbackBytesPerRow == 0) - { - return false; - } - - if (target.Width != targetWidth || target.Height != targetHeight) - { - return false; - } - - CommandEncoder* commandEncoder = this.compositeSessionCommandEncoder; - bool usingSessionCommandEncoder = commandEncoder is not null; - CommandBuffer* commandBuffer = null; - try - { - this.TryCloseCompositeSessionPassLocked(); - - if (commandEncoder is null) - { - CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = gpuState.Api.DeviceCreateCommandEncoder(gpuState.Device, in commandEncoderDescriptor); - if (commandEncoder is null) - { - return false; - } - } - - ImageCopyTexture source = new() - { - Texture = this.compositeSessionTargetTexture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All - }; - - ImageCopyBuffer destination = new() - { - Buffer = this.compositeSessionReadbackBuffer, - Layout = new TextureDataLayout - { - Offset = 0, - BytesPerRow = this.compositeSessionReadbackBytesPerRow, - RowsPerImage = (uint)targetHeight - } - }; - - Extent3D copySize = new((uint)targetWidth, (uint)targetHeight, 1); - gpuState.Api.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); - - CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = gpuState.Api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); - if (commandBuffer is null) - { - return false; - } - - this.compositeSessionCommandEncoder = null; - - gpuState.Api.QueueSubmit(gpuState.Queue, 1, ref commandBuffer); - gpuState.Api.CommandBufferRelease(commandBuffer); - commandBuffer = null; - - bool readbackSuccess = this.TryReadBackBufferToRegionLocked( - this.compositeSessionReadbackBuffer, - checked((int)this.compositeSessionReadbackBytesPerRow), - target); - - if (!readbackSuccess) - { - Trace("TryFlushCompositeSessionLocked: readback failed"); - return false; - } - - Trace("TryFlushCompositeSessionLocked: completed"); - return true; - } - finally - { - if (usingSessionCommandEncoder) - { - this.compositeSessionCommandEncoder = null; - } - - if (commandBuffer is not null) - { - gpuState.Api.CommandBufferRelease(commandBuffer); - } - - if (commandEncoder is not null) - { - if (this.compositeSessionCommandEncoder == commandEncoder) - { - this.compositeSessionCommandEncoder = null; - } - - gpuState.Api.CommandEncoderRelease(commandEncoder); - } - } - } - - private bool TrySubmitCompositeSessionLocked() - { - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - CommandEncoder* commandEncoder = this.compositeSessionCommandEncoder; - CommandBuffer* commandBuffer = null; - try - { - this.TryCloseCompositeSessionPassLocked(); - - if (commandEncoder is null) - { - return true; - } - - CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = gpuState.Api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); - if (commandBuffer is null) - { - return false; - } - - this.compositeSessionCommandEncoder = null; - gpuState.Api.QueueSubmit(gpuState.Queue, 1, ref commandBuffer); - gpuState.Api.CommandBufferRelease(commandBuffer); - commandBuffer = null; - return true; - } - finally - { - if (commandBuffer is not null) - { - gpuState.Api.CommandBufferRelease(commandBuffer); - } - - if (commandEncoder is not null) - { - gpuState.Api.CommandEncoderRelease(commandEncoder); - } - } - } - - private void ResetCompositeSessionStateLocked() - { - this.TryCloseCompositeSessionPassLocked(); - - if (this.compositeSessionCommandEncoder is not null && this.webGPU is not null) - { - this.webGPU.CommandEncoderRelease(this.compositeSessionCommandEncoder); - this.compositeSessionCommandEncoder = null; - } - - this.compositeSessionTargetRectangle = default; - this.compositeSessionRequiresReadback = false; - this.compositeSessionDirty = false; - this.compositeSessionCommands.Clear(); - } - - private void ReleaseCompositeSessionResourcesLocked() - { - if (this.compositeSessionPassEncoder is not null && this.webGPU is not null) - { - this.webGPU.RenderPassEncoderRelease(this.compositeSessionPassEncoder); - this.compositeSessionPassEncoder = null; - } - - if (this.compositeSessionCommandEncoder is not null && this.webGPU is not null) - { - this.webGPU.CommandEncoderRelease(this.compositeSessionCommandEncoder); - this.compositeSessionCommandEncoder = null; - } - - this.ReleaseAllCoverageCompositeBindGroupsLocked(); - this.ReleaseBufferLocked(this.compositeSessionInstanceBuffer); - this.ReleaseBufferLocked(this.compositeSessionReadbackBuffer); - if (this.compositeSessionOwnsTargetView) - { - this.ReleaseTextureViewLocked(this.compositeSessionTargetView); - } - - this.ReleaseTextureLocked(this.compositeSessionTargetTexture); - this.compositeSessionInstanceBuffer = null; - this.compositeSessionInstanceBufferCapacity = 0; - this.compositeSessionInstanceScratch = null; - this.compositeSessionReadbackBuffer = null; - this.compositeSessionTargetTexture = null; - this.compositeSessionTargetView = null; - this.compositeSessionRequiresReadback = false; - this.compositeSessionOwnsTargetView = false; - this.compositeSessionReadbackBytesPerRow = 0; - this.compositeSessionReadbackByteCount = 0; - this.compositeSessionResourceWidth = 0; - this.compositeSessionResourceHeight = 0; - this.compositeSessionResourceTextureFormat = TextureFormat.Undefined; - this.compositeSessionCommands.Clear(); - } - - private void QueueCompositeCoverageLocked( - CoverageEntry entry, - in Rectangle targetBounds, - in Rectangle destinationRegion, - Point sourceOffset, - WebGPUBrushData brushData, - float blendPercentage) - { - int destinationX = targetBounds.X + destinationRegion.X - this.compositeSessionTargetRectangle.X; - int destinationY = targetBounds.Y + destinationRegion.Y - this.compositeSessionTargetRectangle.Y; - - this.compositeSessionCommands.Add(new GPUCompositeCommand( - entry, - sourceOffset, - brushData, - blendPercentage, - destinationX, - destinationY, - destinationRegion.Width, - destinationRegion.Height)); - this.compositeSessionDirty = true; - } - - private bool TryDrainQueuedCompositeCommandsLocked() - { - if (!this.compositeSessionGPUActive || this.compositeSessionCommands.Count == 0) - { - return true; - } - - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - if (!this.TryEnsureCompositeSessionCommandEncoderLocked(in gpuState)) - { - return false; - } - - RenderPipeline* compositePipeline = this.GetCompositeSessionPipelineLocked(); - if (compositePipeline is null || this.compositeSessionTargetView is null) - { - return false; - } - - int sessionTargetWidth = this.compositeSessionTargetRectangle.Width; - int sessionTargetHeight = this.compositeSessionTargetRectangle.Height; - - int i = 0; - while (i < this.compositeSessionCommands.Count) - { - GPUCompositeCommand firstCommand = this.compositeSessionCommands[i]; - CoverageEntry entry = firstCommand.Coverage; - - int runStart = i; - i++; - while (i < this.compositeSessionCommands.Count && - ReferenceEquals(this.compositeSessionCommands[i].Coverage, entry)) - { - i++; - } - - int runCount = i - runStart; - nuint instanceDataSize = (nuint)(runCount * Unsafe.SizeOf()); - if (!this.TryEnsureCompositeSessionInstanceBufferCapacityLocked(in gpuState, instanceDataSize)) - { - return false; - } - - Span instances = this.GetCompositeInstanceScratch(runCount); - for (int instanceIndex = 0; instanceIndex < runCount; instanceIndex++) - { - GPUCompositeCommand command = this.compositeSessionCommands[runStart + instanceIndex]; - instances[instanceIndex] = new CompositeInstanceData - { - SourceOffsetX = (uint)command.SourceOffset.X, - SourceOffsetY = (uint)command.SourceOffset.Y, - DestinationX = (uint)command.DestinationX, - DestinationY = (uint)command.DestinationY, - DestinationWidth = (uint)command.CompositeWidth, - DestinationHeight = (uint)command.CompositeHeight, - TargetWidth = (uint)sessionTargetWidth, - TargetHeight = (uint)sessionTargetHeight, - BrushKind = (uint)command.BrushData.Kind, - SolidBrushColor = command.BrushData.SolidColor, - BlendPercentage = command.BlendPercentage - }; - } - - fixed (CompositeInstanceData* instancePtr = instances) - { - gpuState.Api.QueueWriteBuffer( - gpuState.Queue, - this.compositeSessionInstanceBuffer, - 0, - instancePtr, - instanceDataSize); - } - - if (!this.TryRunCompositePassLocked( - in gpuState, - this.compositeSessionCommandEncoder, - compositePipeline, - entry, - this.compositeSessionTargetView, - (uint)runCount)) - { - return false; - } - } - - this.compositeSessionCommands.Clear(); - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Span GetCompositeInstanceScratch(int count) - { - if (this.compositeSessionInstanceScratch is null || this.compositeSessionInstanceScratch.Length < count) - { - this.compositeSessionInstanceScratch = new CompositeInstanceData[Math.Max(256, count)]; - } - - return this.compositeSessionInstanceScratch.AsSpan(0, count); - } - - private bool TryEnsureCompositeSessionCommandEncoderLocked(in GPUState gpuState) - { - if (this.compositeSessionCommandEncoder is not null) - { - return true; - } - - CommandEncoderDescriptor commandEncoderDescriptor = default; - this.compositeSessionCommandEncoder = gpuState.Api.DeviceCreateCommandEncoder(gpuState.Device, in commandEncoderDescriptor); - return this.compositeSessionCommandEncoder is not null; - } - - private void TryCloseCompositeSessionPassLocked() - { - if (this.compositeSessionPassEncoder is null) - { - return; - } - - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return; - } - - gpuState.Api.RenderPassEncoderEnd(this.compositeSessionPassEncoder); - gpuState.Api.RenderPassEncoderRelease(this.compositeSessionPassEncoder); - this.compositeSessionPassEncoder = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private RenderPipeline* GetCompositeSessionPipelineLocked() - { - if (this.compositeSessionResourceTextureFormat == TextureFormat.Undefined) - { - return null; - } - - return this.TryGetOrCreateCompositePipelineLocked(this.compositeSessionResourceTextureFormat, out RenderPipeline* pipeline) - ? pipeline - : null; - } - - private BindGroup* GetOrCreateCoverageBindGroupLocked( - in GPUState gpuState, - CoverageEntry coverageEntry, - WgpuBuffer* instanceBuffer, - nuint instanceBufferSize) - { - if (this.compositeBindGroupLayout is null || - coverageEntry.GPUCoverageView is null || - instanceBuffer is null || - instanceBufferSize == 0) - { - return null; - } - - if (coverageEntry.GPUCompositeBindGroup is not null && - coverageEntry.GPUCompositeInstanceBuffer == instanceBuffer) - { - return coverageEntry.GPUCompositeBindGroup; - } - - this.ReleaseCoverageCompositeBindGroupLocked(coverageEntry); - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageEntry.GPUCoverageView - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = instanceBuffer, - Offset = 0, - Size = instanceBufferSize - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = this.compositeBindGroupLayout, - EntryCount = 2, - Entries = bindGroupEntries - }; - - BindGroup* bindGroup = gpuState.Api.DeviceCreateBindGroup(gpuState.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - return null; - } - - coverageEntry.GPUCompositeBindGroup = bindGroup; - coverageEntry.GPUCompositeInstanceBuffer = instanceBuffer; - return bindGroup; - } - - /// - /// Executes one composition draw call into the session target texture. - /// - private bool TryRunCompositePassLocked( - in GPUState gpuState, - CommandEncoder* commandEncoder, - RenderPipeline* compositePipeline, - CoverageEntry coverageEntry, - TextureView* targetView, - uint instanceCount) - { - if (compositePipeline is null || - this.compositeBindGroupLayout is null || - coverageEntry.GPUCoverageView is null || - targetView is null) - { - return false; - } - - if (instanceCount == 0) - { - return true; - } - - if (this.compositeSessionInstanceBuffer is null) + int pixelSizeInBytes = Unsafe.SizeOf(); + ImageCopyTexture destination = new() { - return false; - } + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; - BindGroup* bindGroup = this.GetOrCreateCoverageBindGroupLocked( - in gpuState, - coverageEntry, - this.compositeSessionInstanceBuffer, - this.compositeSessionInstanceBufferCapacity); - if (bindGroup is null) - { - return false; - } + Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); - if (this.compositeSessionPassEncoder is null) + if (IsSingleMemory(sourceRegion.Buffer) && + sourceRegion.Rectangle.X == 0 && + sourceRegion.Width == sourceRegion.Buffer.Width) { - RenderPassColorAttachment colorAttachment = new() - { - View = targetView, - ResolveTarget = null, - LoadOp = LoadOp.Load, - StoreOp = StoreOp.Store, - ClearValue = default - }; + int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); + int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); - RenderPassDescriptor renderPassDescriptor = new() + TextureDataLayout layout = new() { - ColorAttachmentCount = 1, - ColorAttachments = &colorAttachment + Offset = 0, + BytesPerRow = (uint)sourceStrideBytes, + RowsPerImage = (uint)sourceRegion.Height }; - this.compositeSessionPassEncoder = gpuState.Api.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); - if (this.compositeSessionPassEncoder is null) + Span firstRow = sourceRegion.DangerousGetRowSpan(0); + fixed (TPixel* uploadPtr = firstRow) { - return false; + api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); } - } - - gpuState.Api.RenderPassEncoderSetPipeline(this.compositeSessionPassEncoder, compositePipeline); - gpuState.Api.RenderPassEncoderSetBindGroup(this.compositeSessionPassEncoder, 0, bindGroup, 0, null); - gpuState.Api.RenderPassEncoderDraw(this.compositeSessionPassEncoder, CompositeVertexCount, instanceCount, 0, 0); - return true; - } - - private bool TryMapReadBufferLocked(WgpuBuffer* readbackBuffer, nuint byteCount, out byte* mappedData) - { - mappedData = null; - - if (!this.TryGetGPUState(out GPUState gpuState)) - { - return false; - } - - if (readbackBuffer is null) - { - return false; - } - - Trace($"TryReadBackBufferLocked: begin bytes={byteCount}"); - BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; - using ManualResetEventSlim callbackReady = new(false); - void Callback(BufferMapAsyncStatus status, void* userDataPtr) - { - mapStatus = status; - _ = userDataPtr; - callbackReady.Set(); - } - - using PfnBufferMapCallback callbackPtr = PfnBufferMapCallback.From(Callback); - gpuState.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); - - if (!this.WaitForSignalLocked(callbackReady) || mapStatus != BufferMapAsyncStatus.Success) - { - Trace($"TryReadBackBufferLocked: map failed status={mapStatus}"); - return false; - } - - Trace("TryReadBackBufferLocked: map callback success"); - void* rawMappedData = gpuState.Api.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); - if (rawMappedData is null) - { - gpuState.Api.BufferUnmap(readbackBuffer); - Trace("TryReadBackBufferLocked: mapped range null"); - return false; - } - - mappedData = (byte*)rawMappedData; - return true; - } - private bool TryReadBackBufferToRegionLocked( - WgpuBuffer* readbackBuffer, - int sourceRowBytes, - Buffer2DRegion destinationRegion) - where TPixel : unmanaged - { - if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) - { return true; } - int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); - int readbackByteCount = checked(sourceRowBytes * destinationRegion.Height); - if (!this.TryMapReadBufferLocked(readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) - { - return false; - } - + int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + int packedByteCount = checked(packedRowBytes * sourceRegion.Height); + byte[] rented = ArrayPool.Shared.Rent(packedByteCount); try { - ReadOnlySpan sourceData = new(mappedData, readbackByteCount); - int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); - - // If the target region spans full rows in a contiguous backing buffer we can copy - // the mapped data in one block instead of per-row. - if (destinationRegion.Rectangle.X == 0 && - sourceRowBytes == destinationStrideBytes && - TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) + Span packedData = rented.AsSpan(0, packedByteCount); + for (int y = 0; y < sourceRegion.Height; y++) { - Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); - int destinationStart = checked(destinationRegion.Rectangle.Y * destinationStrideBytes); - int copyByteCount = checked(destinationStrideBytes * destinationRegion.Height); - if (destinationBytes.Length >= destinationStart + copyByteCount) - { - sourceData[..copyByteCount].CopyTo(destinationBytes.Slice(destinationStart, copyByteCount)); - return true; - } + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); } - for (int y = 0; y < destinationRegion.Height; y++) + TextureDataLayout layout = new() { - ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); - MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); + Offset = 0, + BytesPerRow = (uint)packedRowBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + fixed (byte* uploadPtr = packedData) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); } return true; } finally { - if (this.TryGetGPUState(out GPUState gpuState)) - { - gpuState.Api.BufferUnmap(readbackBuffer); - } - - Trace("TryReadBackBufferLocked: completed"); + ArrayPool.Shared.Return(rented); } } - private void ReleaseCoverageTextureLocked(CoverageEntry entry) - { - this.ReleaseCoverageCompositeBindGroupLocked(entry); - Trace($"ReleaseCoverageTextureLocked: tex={(nint)entry.GPUCoverageTexture:X} view={(nint)entry.GPUCoverageView:X}"); - this.ReleaseTextureViewLocked(entry.GPUCoverageView); - this.ReleaseTextureLocked(entry.GPUCoverageTexture); - entry.GPUCoverageView = null; - entry.GPUCoverageTexture = null; - } - - private void ReleaseCoverageCompositeBindGroupLocked(CoverageEntry entry) + /// + /// Releases all cached shared WebGPU resources and fallback staging resources. + /// + public void Dispose() { - if (entry.GPUCompositeBindGroup is not null && this.TryGetGPUState(out GPUState gpuState)) + if (this.isDisposed) { - gpuState.Api.BindGroupRelease(entry.GPUCompositeBindGroup); + return; } - entry.GPUCompositeBindGroup = null; - entry.GPUCompositeInstanceBuffer = null; - } + WebGPUFlushContext.ClearDeviceStateCache(); + WebGPUFlushContext.ClearFallbackStagingCache(); - private void ReleaseAllCoverageCompositeBindGroupsLocked() - { - foreach (KeyValuePair kv in this.coverageCache) - { - this.ReleaseCoverageCompositeBindGroupLocked(kv.Value); - } + this.TestingLiveCoverageCount = 0; + this.TestingIsGPUReady = false; + this.TestingGPUInitializationAttempted = false; + this.isDisposed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) @@ -2018,161 +686,25 @@ private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memo } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetGPUState(out GPUState state) - { - if (this.webGPU is null || this.device is null || this.queue is null) - { - state = default; - return false; - } - - state = new GPUState(this.webGPU, this.device, this.queue); - return true; - } - - private void ReleaseTextureViewLocked(TextureView* textureView) - { - if (textureView is null || !this.TryGetGPUState(out GPUState gpuState)) - { - return; - } - - gpuState.Api.TextureViewRelease(textureView); - } - - private void ReleaseTextureLocked(Texture* texture) - { - if (texture is null || !this.TryGetGPUState(out GPUState gpuState)) - { - return; - } - - gpuState.Api.TextureRelease(texture); - } - - private void ReleaseBufferLocked(WgpuBuffer* buffer) - { - if (buffer is null || !this.TryGetGPUState(out GPUState gpuState)) - { - return; - } - - gpuState.Api.BufferRelease(buffer); - } - - private void TryDestroyAndDrainDeviceLocked() - { - if (this.webGPU is null || this.device is null) - { - return; - } - - this.webGPU.DeviceDestroy(this.device); - - if (this.wgpuExtension is not null) - { - // Drain native callbacks/work queues before releasing the device and unloading. - _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); - _ = this.wgpuExtension.DevicePoll(this.device, true, (WrappedSubmissionIndex*)null); - return; - } - - if (this.instance is not null) - { - this.webGPU.InstanceProcessEvents(this.instance); - this.webGPU.InstanceProcessEvents(this.instance); - } - } - - private void ReleaseGPUResourcesLocked() + private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEventSlim signal) { - Trace("ReleaseGPUResourcesLocked: begin"); - this.ResetCompositeSessionStateLocked(); - this.ReleaseCompositeSessionResourcesLocked(); - - foreach (KeyValuePair kv in this.coverageCache) + Wgpu? extension = flushContext.RuntimeLease.WgpuExtension; + if (extension is null) { - this.ReleaseCoverageTextureLocked(kv.Value); - kv.Value.Dispose(); + return signal.Wait(CallbackTimeoutMilliseconds); } - this.coverageCache.Clear(); - - if (this.webGPU is not null) + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) { - this.coverageRasterizer?.Release(); - this.coverageRasterizer = null; - - foreach (KeyValuePair compositePipelineEntry in this.compositePipelines) - { - if (compositePipelineEntry.Value != 0) - { - this.webGPU.RenderPipelineRelease((RenderPipeline*)compositePipelineEntry.Value); - } - } - - this.compositePipelines.Clear(); - - if (this.compositePipelineLayout is not null) - { - this.webGPU.PipelineLayoutRelease(this.compositePipelineLayout); - this.compositePipelineLayout = null; - } - - if (this.compositeBindGroupLayout is not null) - { - this.webGPU.BindGroupLayoutRelease(this.compositeBindGroupLayout); - this.compositeBindGroupLayout = null; - } - - if (this.device is not null) - { - this.TryDestroyAndDrainDeviceLocked(); - } - - if (this.queue is not null) - { - this.webGPU.QueueRelease(this.queue); - this.queue = null; - } - - if (this.device is not null) - { - this.webGPU.DeviceRelease(this.device); - this.device = null; - } - - if (this.adapter is not null) - { - this.webGPU.AdapterRelease(this.adapter); - this.adapter = null; - } - - if (this.instance is not null) - { - this.webGPU.InstanceRelease(this.instance); - this.instance = null; - } - - this.webGPU = null; + _ = extension.DevicePoll(flushContext.Device, true, (WrappedSubmissionIndex*)null); } - this.wgpuExtension = null; - this.runtimeLease?.Dispose(); - this.runtimeLease = null; - this.liveCoverageCount = 0; - this.IsGPUReady = false; - this.compositeSessionGPUActive = false; - this.compositeSessionDepth = 0; - Trace("ReleaseGPUResourcesLocked: end"); + return signal.IsSet; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ThrowIfDisposed() - => ObjectDisposedException.ThrowIf(this.isDisposed, this); - [StructLayout(LayoutKind.Sequential)] - private struct CompositeInstanceData + internal struct CompositeInstanceData { public uint SourceOffsetX; public uint SourceOffsetY; @@ -2192,85 +724,4 @@ private struct CompositeInstanceData public float Padding4; public float Padding5; } - - [StructLayout(LayoutKind.Sequential)] - private readonly struct GPUCompositeCommand - { - public GPUCompositeCommand( - CoverageEntry coverage, - Point sourceOffset, - WebGPUBrushData brushData, - float blendPercentage, - int destinationX, - int destinationY, - int compositeWidth, - int compositeHeight) - { - this.Coverage = coverage; - this.SourceOffset = sourceOffset; - this.BrushData = brushData; - this.BlendPercentage = blendPercentage; - this.DestinationX = destinationX; - this.DestinationY = destinationY; - this.CompositeWidth = compositeWidth; - this.CompositeHeight = compositeHeight; - } - - public CoverageEntry Coverage { get; } - - public Point SourceOffset { get; } - - public WebGPUBrushData BrushData { get; } - - public float BlendPercentage { get; } - - public int DestinationX { get; } - - public int DestinationY { get; } - - public int CompositeWidth { get; } - - public int CompositeHeight { get; } - } - - private readonly struct GPUState - { - public GPUState(WebGPU api, Device* device, Queue* queue) - { - this.Api = api; - this.Device = device; - this.Queue = queue; - } - - public WebGPU Api { get; } - - public Device* Device { get; } - - public Queue* Queue { get; } - } - - private sealed class CoverageEntry : IDisposable - { - public CoverageEntry(int width, int height) - { - this.Width = width; - this.Height = height; - } - - public int Width { get; } - - public int Height { get; } - - public Texture* GPUCoverageTexture { get; set; } - - public TextureView* GPUCoverageView { get; set; } - - public BindGroup* GPUCompositeBindGroup { get; set; } - - public WgpuBuffer* GPUCompositeInstanceBuffer { get; set; } - - public void Dispose() - { - } - } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs new file mode 100644 index 000000000..8778c90ca --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -0,0 +1,1321 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Per-flush WebGPU execution context created from a single frame target. +/// +internal sealed unsafe class WebGPUFlushContext : IDisposable +{ + private static readonly ConcurrentDictionary FallbackStagingCache = new(); + private static readonly ConcurrentDictionary DeviceStateCache = new(); + private static readonly ConcurrentDictionary CpuReadbackFlushContexts = new(); + private static readonly object SharedHandleSync = new(); + private const int CallbackTimeoutMilliseconds = 10_000; + + private bool disposed; + private bool ownsTargetTexture; + private bool ownsTargetView; + private bool ownsReadbackBuffer; + private readonly List transientBindGroups = []; + + private WebGPUFlushContext( + WebGPURuntime.Lease runtimeLease, + Device* device, + Queue* queue, + in Rectangle targetBounds, + TextureFormat textureFormat, + DeviceSharedState deviceState) + { + this.RuntimeLease = runtimeLease; + this.Api = runtimeLease.Api; + this.Device = device; + this.Queue = queue; + this.TargetBounds = targetBounds; + this.TextureFormat = textureFormat; + this.DeviceState = deviceState; + } + + public WebGPURuntime.Lease RuntimeLease { get; } + + public WebGPU Api { get; } + + public Device* Device { get; } + + public Queue* Queue { get; } + + public Rectangle TargetBounds { get; } + + public TextureFormat TextureFormat { get; } + + public DeviceSharedState DeviceState { get; } + + public Texture* TargetTexture { get; private set; } + + public TextureView* TargetView { get; private set; } + + public bool RequiresReadback { get; private set; } + + public WgpuBuffer* ReadbackBuffer { get; private set; } + + public uint ReadbackBytesPerRow { get; private set; } + + public ulong ReadbackByteCount { get; private set; } + + public WgpuBuffer* InstanceBuffer { get; private set; } + + public nuint InstanceBufferCapacity { get; private set; } + + public CommandEncoder* CommandEncoder { get; set; } + + public RenderPassEncoder* PassEncoder { get; private set; } + + public static WebGPUFlushContext Create( + ICanvasFrame frame, + TextureFormat expectedTextureFormat, + int pixelSizeInBytes) + where TPixel : unmanaged, IPixel + { + WebGPUSurfaceCapability? nativeCapability = TryGetNativeSurfaceCapability(frame, expectedTextureFormat); + WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + try + { + Device* device; + Queue* queue; + TextureFormat textureFormat; + Rectangle bounds = frame.Bounds; + DeviceSharedState deviceState; + WebGPUFlushContext context; + + if (nativeCapability is not null) + { + device = (Device*)nativeCapability.Device; + queue = (Queue*)nativeCapability.Queue; + textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); + deviceState = GetOrCreateDeviceState(lease.Api, device); + context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, deviceState); + context.InitializeNativeTarget(nativeCapability); + return context; + } + + if (!frame.TryGetCpuRegion(out Buffer2DRegion cpuRegion)) + { + throw new NotSupportedException("Frame does not expose a GPU-native surface or CPU region."); + } + + if (!TryGetOrCreateSharedHandles(lease.Api, out device, out queue, out string? error)) + { + throw new InvalidOperationException(error ?? "WebGPU shared handles are unavailable."); + } + + deviceState = GetOrCreateDeviceState(lease.Api, device); + context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, deviceState); + context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes); + return context; + } + catch + { + lease.Dispose(); + throw; + } + } + + public static WebGPUFlushContext CreateUploadContext(ICanvasFrame frame) + where TPixel : unmanaged, IPixel + { + WebGPUSurfaceCapability? nativeCapability = + TryGetWritableNativeSurfaceCapability(frame) + ?? throw new NotSupportedException("Fallback upload requires a native WebGPU surface exposing writable device, queue, and texture handles."); + + WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + try + { + Rectangle bounds = frame.Bounds; + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); + Device* device = (Device*)nativeCapability.Device; + DeviceSharedState deviceState = GetOrCreateDeviceState(lease.Api, device); + WebGPUFlushContext context = new( + lease, + device, + (Queue*)nativeCapability.Queue, + in bounds, + textureFormat, + deviceState); + context.InitializeNativeTarget(nativeCapability); + return context; + } + catch + { + lease.Dispose(); + throw; + } + } + + public static FallbackStagingLease RentFallbackStaging(MemoryAllocator allocator, in Rectangle bounds) + where TPixel : unmanaged, IPixel + { + IDisposable entry = FallbackStagingCache.GetOrAdd( + typeof(TPixel), + static _ => new FallbackStagingEntry()); + + return ((FallbackStagingEntry)entry).Rent(allocator, in bounds); + } + + public static void ClearFallbackStagingCache() + { + foreach (IDisposable entry in FallbackStagingCache.Values) + { + entry.Dispose(); + } + + FallbackStagingCache.Clear(); + } + + public static void ClearDeviceStateCache() + { + foreach (WebGPUFlushContext context in CpuReadbackFlushContexts.Values) + { + context.Dispose(); + } + + CpuReadbackFlushContexts.Clear(); + + foreach (DeviceSharedState state in DeviceStateCache.Values) + { + state.Dispose(); + } + + DeviceStateCache.Clear(); + } + + public static WebGPUFlushContext GetOrCreateCpuReadbackFlushContext( + int flushId, + ICanvasFrame frame, + TextureFormat expectedTextureFormat, + int pixelSizeInBytes, + out bool fromCache) + where TPixel : unmanaged, IPixel + { + if (CpuReadbackFlushContexts.TryGetValue(flushId, out WebGPUFlushContext? cached)) + { + fromCache = true; + return cached; + } + + fromCache = false; + WebGPUFlushContext created = Create(frame, expectedTextureFormat, pixelSizeInBytes); + if (!created.RequiresReadback) + { + return created; + } + + if (CpuReadbackFlushContexts.TryAdd(flushId, created)) + { + return created; + } + + created.Dispose(); + fromCache = true; + return CpuReadbackFlushContexts[flushId]; + } + + public static void CompleteCpuReadbackFlushContext(int flushId) + { + if (CpuReadbackFlushContexts.TryRemove(flushId, out WebGPUFlushContext? context)) + { + context.Dispose(); + } + } + + public static bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle, out string? error) + { + if (WebGPURuntime.TryGetSharedHandles(out Device* sharedDevice, out Queue* sharedQueue)) + { + deviceHandle = (nint)sharedDevice; + queueHandle = (nint)sharedQueue; + error = null; + return true; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (TryGetOrCreateSharedHandles(lease.Api, out Device* device, out Queue* queue, out error)) + { + deviceHandle = (nint)device; + queueHandle = (nint)queue; + return true; + } + + deviceHandle = 0; + queueHandle = 0; + return false; + } + + public bool EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapacityBytes) + { + if (this.InstanceBuffer is not null && this.InstanceBufferCapacity >= requiredBytes) + { + return true; + } + + if (this.InstanceBuffer is not null) + { + this.Api.BufferRelease(this.InstanceBuffer); + this.InstanceBuffer = null; + this.InstanceBufferCapacity = 0; + } + + nuint targetSize = requiredBytes > minimumCapacityBytes ? requiredBytes : minimumCapacityBytes; + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = targetSize + }; + + this.InstanceBuffer = this.Api.DeviceCreateBuffer(this.Device, in descriptor); + if (this.InstanceBuffer is null) + { + return false; + } + + this.InstanceBufferCapacity = targetSize; + return true; + } + + public bool EnsureCommandEncoder() + { + if (this.CommandEncoder is not null) + { + return true; + } + + CommandEncoderDescriptor descriptor = default; + this.CommandEncoder = this.Api.DeviceCreateCommandEncoder(this.Device, in descriptor); + return this.CommandEncoder is not null; + } + + public bool BeginRenderPass() + { + if (this.PassEncoder is not null) + { + return true; + } + + if (this.CommandEncoder is null || this.TargetView is null) + { + return false; + } + + RenderPassColorAttachment colorAttachment = new() + { + View = this.TargetView, + ResolveTarget = null, + LoadOp = LoadOp.Load, + StoreOp = StoreOp.Store, + ClearValue = default + }; + + RenderPassDescriptor renderPassDescriptor = new() + { + ColorAttachmentCount = 1, + ColorAttachments = &colorAttachment + }; + + this.PassEncoder = this.Api.CommandEncoderBeginRenderPass(this.CommandEncoder, in renderPassDescriptor); + return this.PassEncoder is not null; + } + + public void EndRenderPassIfOpen() + { + if (this.PassEncoder is null) + { + return; + } + + this.Api.RenderPassEncoderEnd(this.PassEncoder); + this.Api.RenderPassEncoderRelease(this.PassEncoder); + this.PassEncoder = null; + } + + public void TrackBindGroup(BindGroup* bindGroup) + { + if (bindGroup is not null) + { + this.transientBindGroups.Add((nint)bindGroup); + } + } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.EndRenderPassIfOpen(); + + if (this.CommandEncoder is not null) + { + this.Api.CommandEncoderRelease(this.CommandEncoder); + this.CommandEncoder = null; + } + + if (this.InstanceBuffer is not null) + { + this.Api.BufferRelease(this.InstanceBuffer); + this.InstanceBuffer = null; + this.InstanceBufferCapacity = 0; + } + + if (this.ownsReadbackBuffer && this.ReadbackBuffer is not null) + { + this.Api.BufferRelease(this.ReadbackBuffer); + } + + if (this.ownsTargetView && this.TargetView is not null) + { + this.Api.TextureViewRelease(this.TargetView); + } + + if (this.ownsTargetTexture && this.TargetTexture is not null) + { + this.Api.TextureRelease(this.TargetTexture); + } + + for (int i = 0; i < this.transientBindGroups.Count; i++) + { + this.Api.BindGroupRelease((BindGroup*)this.transientBindGroups[i]); + } + + this.transientBindGroups.Clear(); + + this.ReadbackBuffer = null; + this.TargetView = null; + this.TargetTexture = null; + this.ReadbackBytesPerRow = 0; + this.ReadbackByteCount = 0; + this.RequiresReadback = false; + this.ownsReadbackBuffer = false; + this.ownsTargetView = false; + this.ownsTargetTexture = false; + + this.RuntimeLease.Dispose(); + this.disposed = true; + } + + private static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* device) + { + nint cacheKey = (nint)device; + if (DeviceStateCache.TryGetValue(cacheKey, out DeviceSharedState? existing)) + { + return existing; + } + + DeviceSharedState created = new(api, device); + if (DeviceStateCache.TryAdd(cacheKey, created)) + { + return created; + } + + created.Dispose(); + return DeviceStateCache.TryGetValue(cacheKey, out DeviceSharedState? winner) + ? winner + : GetOrCreateDeviceState(api, device); + } + + private static bool TryGetOrCreateSharedHandles( + WebGPU api, + out Device* device, + out Queue* queue, + out string? error) + { + if (WebGPURuntime.TryGetSharedHandles(out device, out queue)) + { + error = null; + return true; + } + + lock (SharedHandleSync) + { + if (WebGPURuntime.TryGetSharedHandles(out device, out queue)) + { + error = null; + return true; + } + + Instance* instance = api.CreateInstance((InstanceDescriptor*)null); + if (instance is null) + { + error = "WebGPU.CreateInstance returned null."; + device = null; + queue = null; + return false; + } + + Adapter* adapter = null; + Device* requestedDevice = null; + Queue* requestedQueue = null; + bool initialized = false; + try + { + if (!TryRequestAdapter(api, instance, out adapter, out error)) + { + device = null; + queue = null; + return false; + } + + if (!TryRequestDevice(api, adapter, out requestedDevice, out error)) + { + device = null; + queue = null; + return false; + } + + requestedQueue = api.DeviceGetQueue(requestedDevice); + if (requestedQueue is null) + { + error = "WebGPU.DeviceGetQueue returned null."; + device = null; + queue = null; + return false; + } + + WebGPURuntime.SetSharedHandles((nint)requestedDevice, (nint)requestedQueue); + device = requestedDevice; + queue = requestedQueue; + error = null; + initialized = true; + return true; + } + finally + { + if (adapter is not null) + { + api.AdapterRelease(adapter); + } + + api.InstanceRelease(instance); + + if (!initialized) + { + if (requestedQueue is not null) + { + api.QueueRelease(requestedQueue); + } + + if (requestedDevice is not null) + { + api.DeviceRelease(requestedDevice); + } + } + } + } + } + + private static bool TryRequestAdapter(WebGPU api, Instance* instance, out Adapter* adapter, out string? error) + { + RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; + Adapter* callbackAdapter = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) + { + callbackStatus = status; + callbackAdapter = adapterPtr; + callbackReady.Set(); + } + + using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); + RequestAdapterOptions options = new() + { + PowerPreference = PowerPreference.HighPerformance + }; + + api.InstanceRequestAdapter(instance, in options, callbackPtr, null); + if (!WaitForSignal(callbackReady)) + { + adapter = null; + error = "Timed out while waiting for WebGPU adapter request callback."; + return false; + } + + adapter = callbackAdapter; + if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) + { + error = $"WebGPU adapter request failed with status '{callbackStatus}'."; + return false; + } + + error = null; + return true; + } + + private static bool TryRequestDevice(WebGPU api, Adapter* adapter, out Device* device, out string? error) + { + RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; + Device* callbackDevice = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) + { + callbackStatus = status; + callbackDevice = devicePtr; + callbackReady.Set(); + } + + using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); + DeviceDescriptor descriptor = default; + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + if (!WaitForSignal(callbackReady)) + { + device = null; + error = "Timed out while waiting for WebGPU device request callback."; + return false; + } + + device = callbackDevice; + if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) + { + error = $"WebGPU device request failed with status '{callbackStatus}'."; + return false; + } + + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WaitForSignal(ManualResetEventSlim signal) + => signal.Wait(CallbackTimeoutMilliseconds); + + private void InitializeNativeTarget(WebGPUSurfaceCapability capability) + { + this.TargetTexture = (Texture*)capability.TargetTexture; + this.TargetView = (TextureView*)capability.TargetTextureView; + this.RequiresReadback = false; + this.ReadbackBuffer = null; + this.ReadbackBytesPerRow = 0; + this.ReadbackByteCount = 0; + this.ownsTargetTexture = false; + this.ownsTargetView = false; + this.ownsReadbackBuffer = false; + } + + private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int pixelSizeInBytes) + where TPixel : unmanaged + { + int width = cpuRegion.Width; + int height = cpuRegion.Height; + uint textureRowBytes = checked((uint)width * (uint)pixelSizeInBytes); + uint readbackRowBytes = AlignTo256(textureRowBytes); + ulong readbackByteCount = checked((ulong)readbackRowBytes * (uint)height); + + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = this.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* targetTexture = this.Api.DeviceCreateTexture(this.Device, in targetTextureDescriptor); + if (targetTexture is null) + { + throw new InvalidOperationException("Failed to create CPU flush target texture."); + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = this.TextureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* targetView = this.Api.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + this.Api.TextureRelease(targetTexture); + throw new InvalidOperationException("Failed to create CPU flush target view."); + } + + BufferDescriptor readbackDescriptor = new() + { + Usage = BufferUsage.MapRead | BufferUsage.CopyDst, + Size = readbackByteCount + }; + + WgpuBuffer* readbackBuffer = this.Api.DeviceCreateBuffer(this.Device, in readbackDescriptor); + if (readbackBuffer is null) + { + this.Api.TextureViewRelease(targetView); + this.Api.TextureRelease(targetTexture); + throw new InvalidOperationException("Failed to create CPU flush readback buffer."); + } + + try + { + QueueWriteTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion); + } + catch + { + this.Api.BufferRelease(readbackBuffer); + this.Api.TextureViewRelease(targetView); + this.Api.TextureRelease(targetTexture); + throw; + } + + this.TargetTexture = targetTexture; + this.TargetView = targetView; + this.ReadbackBuffer = readbackBuffer; + this.ReadbackBytesPerRow = readbackRowBytes; + this.ReadbackByteCount = readbackByteCount; + this.RequiresReadback = true; + this.ownsTargetTexture = true; + this.ownsTargetView = true; + this.ownsReadbackBuffer = true; + } + + private static WebGPUSurfaceCapability? TryGetNativeSurfaceCapability(ICanvasFrame frame, TextureFormat expectedTextureFormat) + where TPixel : unmanaged, IPixel + { + if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability)) + { + return null; + } + + if (capability.Device == 0 || + capability.Queue == 0 || + capability.TargetTextureView == 0 || + WebGPUTextureFormatMapper.ToSilk(capability.TargetFormat) != expectedTextureFormat) + { + return null; + } + + Rectangle bounds = frame.Bounds; + if (bounds.X < 0 || + bounds.Y < 0 || + bounds.Right > capability.Width || + bounds.Bottom > capability.Height) + { + return null; + } + + return capability; + } + + private static WebGPUSurfaceCapability? TryGetWritableNativeSurfaceCapability(ICanvasFrame frame) + where TPixel : unmanaged, IPixel + { + if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability)) + { + return null; + } + + if (capability.Device == 0 || capability.Queue == 0 || capability.TargetTexture == 0) + { + return null; + } + + Rectangle bounds = frame.Bounds; + if (bounds.X < 0 || + bounds.Y < 0 || + bounds.Right > capability.Width || + bounds.Bottom > capability.Height) + { + return null; + } + + return capability; + } + + private static void QueueWriteTextureFromRegion( + WebGPU api, + Queue* queue, + Texture* destinationTexture, + Buffer2DRegion sourceRegion) + where TPixel : unmanaged + { + int pixelSizeInBytes = Unsafe.SizeOf(); + ImageCopyTexture destination = new() + { + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + + if (sourceRegion.Rectangle.X == 0 && + sourceRegion.Width == sourceRegion.Buffer.Width && + sourceRegion.Buffer.MemoryGroup.Count == 1) + { + int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); + int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)sourceStrideBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + Span firstRow = sourceRegion.DangerousGetRowSpan(0); + fixed (TPixel* uploadPtr = firstRow) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); + } + + return; + } + + int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + int packedByteCount = checked(packedRowBytes * sourceRegion.Height); + byte[] rented = ArrayPool.Shared.Rent(packedByteCount); + try + { + Span packedData = rented.AsSpan(0, packedByteCount); + for (int y = 0; y < sourceRegion.Height; y++) + { + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); + } + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)packedRowBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + fixed (byte* uploadPtr = packedData) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); + } + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + + internal sealed class DeviceSharedState : IDisposable + { + private readonly Dictionary coverageCache = []; + private readonly ConcurrentDictionary compositePipelines = new(); + private WebGPURasterizer? coverageRasterizer; + private PipelineLayout* compositePipelineLayout; + private bool disposed; + + internal DeviceSharedState(WebGPU api, Device* device) + { + this.Api = api; + this.Device = device; + } + + private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; + + private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; + + public object SyncRoot { get; } = new(); + + public WebGPU Api { get; } + + public Device* Device { get; } + + public BindGroupLayout* CompositeBindGroupLayout { get; private set; } + + public int CoverageCount => this.coverageCache.Count; + + public bool TryEnsureResources(out string? error) + { + if (this.disposed) + { + error = "WebGPU device state is disposed."; + return false; + } + + if (this.CompositeBindGroupLayout is null || this.compositePipelineLayout is null) + { + if (!this.TryCreateCompositeInfrastructure(out error)) + { + return false; + } + } + + this.coverageRasterizer ??= new WebGPURasterizer(this.Api); + if (!this.coverageRasterizer.IsInitialized && !this.coverageRasterizer.Initialize(this.Device)) + { + error = "Failed to initialize WebGPU coverage rasterizer."; + return false; + } + + error = null; + return true; + } + + public bool TryGetOrCreateCoverageEntry( + in CompositionCoverageDefinition definition, + Queue* queue, + [NotNullWhen(true)] out CoverageEntry? coverageEntry, + out string? error) + { + if (!this.TryEnsureResources(out error)) + { + coverageEntry = null; + return false; + } + + if (this.coverageCache.TryGetValue(definition.DefinitionKey, out CoverageEntry? cached)) + { + coverageEntry = cached; + return true; + } + + RasterizerOptions rasterizerOptions = definition.RasterizerOptions; + if (this.coverageRasterizer is null || + !this.coverageRasterizer.TryCreateCoverageTexture( + definition.Path, + in rasterizerOptions, + this.Device, + queue, + out Texture* coverageTexture, + out TextureView* coverageView)) + { + coverageEntry = null; + error = "Failed to rasterize coverage texture."; + return false; + } + + Size size = rasterizerOptions.Interest.Size; + coverageEntry = new CoverageEntry(size.Width, size.Height) + { + GPUCoverageTexture = coverageTexture, + GPUCoverageView = coverageView + }; + this.coverageCache.Add(definition.DefinitionKey, coverageEntry); + error = null; + return true; + } + + public bool TryGetOrCreateCompositePipeline(TextureFormat textureFormat, out RenderPipeline* pipeline, out string? error) + { + if (!this.TryEnsureResources(out error)) + { + pipeline = null; + return false; + } + + if (this.compositePipelines.TryGetValue(textureFormat, out nint existingHandle) && existingHandle != 0) + { + pipeline = (RenderPipeline*)existingHandle; + return true; + } + + RenderPipeline* created = this.CreateCompositePipelineForFormat(textureFormat); + if (created is null) + { + pipeline = null; + error = $"Failed to create composite pipeline for format '{textureFormat}'."; + return false; + } + + nint createdHandle = (nint)created; + nint cachedHandle = this.compositePipelines.GetOrAdd(textureFormat, createdHandle); + if (cachedHandle != createdHandle) + { + this.Api.RenderPipelineRelease(created); + } + + pipeline = (RenderPipeline*)cachedHandle; + error = null; + return true; + } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + foreach (CoverageEntry entry in this.coverageCache.Values) + { + ReleaseCoverageTexture(this.Api, entry); + } + + this.coverageCache.Clear(); + + this.coverageRasterizer?.Release(); + this.coverageRasterizer = null; + + foreach (KeyValuePair entry in this.compositePipelines) + { + if (entry.Value != 0) + { + this.Api.RenderPipelineRelease((RenderPipeline*)entry.Value); + } + } + + this.compositePipelines.Clear(); + + if (this.compositePipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(this.compositePipelineLayout); + this.compositePipelineLayout = null; + } + + if (this.CompositeBindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(this.CompositeBindGroupLayout); + this.CompositeBindGroupLayout = null; + } + + this.disposed = true; + } + + private bool TryCreateCompositeInfrastructure(out string? error) + { + BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; + layoutEntries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + layoutEntries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Vertex | ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + + BindGroupLayoutDescriptor layoutDescriptor = new() + { + EntryCount = 2, + Entries = layoutEntries + }; + + this.CompositeBindGroupLayout = this.Api.DeviceCreateBindGroupLayout(this.Device, in layoutDescriptor); + if (this.CompositeBindGroupLayout is null) + { + error = "Failed to create composite bind group layout."; + return false; + } + + BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; + bindGroupLayouts[0] = this.CompositeBindGroupLayout; + PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + BindGroupLayoutCount = 1, + BindGroupLayouts = bindGroupLayouts + }; + + this.compositePipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); + if (this.compositePipelineLayout is null) + { + error = "Failed to create composite pipeline layout."; + return false; + } + + error = null; + return true; + } + + private RenderPipeline* CreateCompositePipelineForFormat(TextureFormat textureFormat) + { + if (this.compositePipelineLayout is null) + { + return null; + } + + ShaderModule* shaderModule = null; + try + { + ReadOnlySpan shaderCode = CompositeCoverageShader.Code; + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + shaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); + } + + if (shaderModule is null) + { + return null; + } + + ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; + ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) + { + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + { + return this.CreateCompositePipeline(shaderModule, vertexEntryPointPtr, fragmentEntryPointPtr, textureFormat); + } + } + } + finally + { + if (shaderModule is not null) + { + this.Api.ShaderModuleRelease(shaderModule); + } + } + } + + private RenderPipeline* CreateCompositePipeline( + ShaderModule* shaderModule, + byte* vertexEntryPointPtr, + byte* fragmentEntryPointPtr, + TextureFormat textureFormat) + { + VertexState vertexState = new() + { + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + BlendState blendState = new() + { + Color = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + }, + Alpha = new BlendComponent + { + Operation = BlendOperation.Add, + SrcFactor = BlendFactor.One, + DstFactor = BlendFactor.OneMinusSrcAlpha + } + }; + + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = textureFormat, + Blend = &blendState, + WriteMask = ColorWriteMask.All + }; + + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; + + RenderPipelineDescriptor descriptor = new() + { + Layout = this.compositePipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState + }; + + return this.Api.DeviceCreateRenderPipeline(this.Device, in descriptor); + } + + private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) + { + if (entry.GPUCoverageView is not null) + { + api.TextureViewRelease(entry.GPUCoverageView); + entry.GPUCoverageView = null; + } + + if (entry.GPUCoverageTexture is not null) + { + api.TextureRelease(entry.GPUCoverageTexture); + entry.GPUCoverageTexture = null; + } + } + } + + internal sealed class CoverageEntry + { + public CoverageEntry(int width, int height) + { + this.Width = width; + this.Height = height; + } + + public int Width { get; } + + public int Height { get; } + + public Texture* GPUCoverageTexture { get; set; } + + public TextureView* GPUCoverageView { get; set; } + } + + /// + /// Lease over a CPU fallback staging region. + /// + /// The pixel type of the staging region. + public sealed class FallbackStagingLease : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly FallbackStagingEntry? owner; + private readonly Buffer2D? temporaryBuffer; + private int disposed; + + /// + /// Initializes a new instance of the class. + /// + internal FallbackStagingLease( + Buffer2DRegion region, + FallbackStagingEntry? owner, + Buffer2D? temporaryBuffer) + { + this.Region = region; + this.owner = owner; + this.temporaryBuffer = temporaryBuffer; + } + + /// + /// Gets the staging region for fallback rendering. + /// + public Buffer2DRegion Region { get; } + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.disposed, 1) != 0) + { + return; + } + + this.temporaryBuffer?.Dispose(); + this.owner?.Release(); + } + } + + /// + /// Cached staging entry for one pixel type. + /// + /// The pixel type stored by this entry. + internal sealed class FallbackStagingEntry : IDisposable + where TPixel : unmanaged, IPixel + { + private Buffer2D? buffer; + private Size size; + private int inUse; + + /// + /// Rents a staging lease for the specified bounds. + /// + public FallbackStagingLease Rent(MemoryAllocator allocator, in Rectangle bounds) + { + if (Interlocked.CompareExchange(ref this.inUse, 1, 0) == 0) + { + this.EnsureSize(allocator, bounds.Size); + Buffer2D? current = this.buffer; + if (current is null) + { + this.Release(); + throw new InvalidOperationException("Fallback staging buffer is not initialized."); + } + + return new FallbackStagingLease( + new Buffer2DRegion(current, bounds), + this, + temporaryBuffer: null); + } + + Buffer2D temporary = allocator.Allocate2D(bounds.Size, AllocationOptions.Clean); + return new FallbackStagingLease( + new Buffer2DRegion(temporary, bounds), + owner: null, + temporaryBuffer: temporary); + } + + /// + /// Releases an acquired cached staging entry. + /// + public void Release() + => Volatile.Write(ref this.inUse, 0); + + /// + public void Dispose() + { + this.buffer?.Dispose(); + this.buffer = null; + this.size = default; + this.inUse = 0; + } + + private void EnsureSize(MemoryAllocator allocator, Size requiredSize) + { + if (this.buffer is not null && + this.size.Width >= requiredSize.Width && + this.size.Height >= requiredSize.Height) + { + return; + } + + this.buffer?.Dispose(); + this.buffer = allocator.Allocate2D(requiredSize, AllocationOptions.Clean); + this.size = requiredSize; + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs new file mode 100644 index 000000000..130875fc9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Creates instances for externally-owned WebGPU targets. +/// +public static class WebGPUNativeSurfaceFactory +{ + /// + /// Creates a WebGPU-backed from opaque native handles. + /// + /// Canvas pixel format. + /// Opaque WGPUDevice* handle. + /// Opaque WGPUQueue* handle. + /// Opaque WGPUTexture* handle for writable uploads. + /// Opaque WGPUTextureView* handle for render target binding. + /// Texture format identifier. + /// Surface width in pixels. + /// Surface height in pixels. + /// Whether the surface is sRGB encoded. + /// Whether surface alpha is premultiplied. + /// A configured instance. + public static NativeSurface Create( + nint deviceHandle, + nint queueHandle, + nint targetTextureHandle, + nint targetTextureViewHandle, + WebGPUTextureFormatId targetFormat, + int width, + int height, + bool isSrgb, + bool isPremultipliedAlpha) + where TPixel : unmanaged, IPixel + { + ValidateCommon( + deviceHandle, + queueHandle, + targetTextureViewHandle, + width, + height); + + ValidatePixelCompatibility(targetFormat); + + NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); + nativeSurface.SetCapability(new WebGPUSurfaceCapability( + deviceHandle, + queueHandle, + targetTextureHandle, + targetTextureViewHandle, + targetFormat, + width, + height, + isSrgb, + isPremultipliedAlpha)); + return nativeSurface; + } + + private static void ValidateCommon( + nint deviceHandle, + nint queueHandle, + nint targetTextureViewHandle, + int width, + int height) + { + if (deviceHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(deviceHandle), "Device handle must be non-zero."); + } + + if (queueHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(queueHandle), "Queue handle must be non-zero."); + } + + if (targetTextureViewHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(targetTextureViewHandle), "Texture view handle must be non-zero."); + } + + if (width <= 0) + { + throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than zero."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException(nameof(height), "Height must be greater than zero."); + } + } + + private static void ValidatePixelCompatibility(WebGPUTextureFormatId targetFormat) + where TPixel : unmanaged, IPixel + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId expected)) + { + throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."); + } + + if (expected != targetFormat) + { + throw new ArgumentException( + $"Target format '{targetFormat}' is not compatible with pixel type '{typeof(TPixel).Name}' (expected '{expected}').", + nameof(targetFormat)); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs index 4a9c609d5..19a4d2485 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs @@ -18,8 +18,6 @@ internal sealed unsafe class WebGPURasterizer private const uint CoverageSampleCount = 4; private readonly WebGPU webGPU; - private readonly Device* device; - private readonly Queue* queue; private PipelineLayout* coveragePipelineLayout; private RenderPipeline* coverageStencilEvenOddPipeline; @@ -35,12 +33,7 @@ internal sealed unsafe class WebGPURasterizer private WgpuBuffer* coverageScratchVertexBuffer; private ulong coverageScratchVertexCapacityBytes; - public WebGPURasterizer(WebGPU webGPU, Device* device, Queue* queue) - { - this.webGPU = webGPU; - this.device = device; - this.queue = queue; - } + public WebGPURasterizer(WebGPU webGPU) => this.webGPU = webGPU; private static ReadOnlySpan CoverageStencilVertexEntryPoint => "vs_edge\0"u8; @@ -57,19 +50,21 @@ this.coverageStencilNonZeroIncrementPipeline is not null && this.coverageStencilNonZeroDecrementPipeline is not null && this.coverageCoverPipeline is not null; - public bool Initialize() + public bool Initialize(Device* device) { if (this.IsInitialized) { return true; } - return this.TryCreateCoveragePipelineLocked(); + return this.TryCreateCoveragePipelineLocked(device); } public bool TryCreateCoverageTexture( IPath path, in RasterizerOptions rasterizerOptions, + Device* device, + Queue* queue, out Texture* coverageTexture, out TextureView* coverageView) { @@ -91,7 +86,13 @@ public bool TryCreateCoverageTexture( return false; } - return this.TryRasterizeCoverageTextureLocked(in coverageTriangleData, in rasterizerOptions, out coverageTexture, out coverageView); + return this.TryRasterizeCoverageTextureLocked( + in coverageTriangleData, + in rasterizerOptions, + device, + queue, + out coverageTexture, + out coverageView); } public void Release() @@ -132,7 +133,7 @@ public void Release() /// /// Creates the render pipeline used for coverage rasterization. /// - private bool TryCreateCoveragePipelineLocked() + private bool TryCreateCoveragePipelineLocked(Device* device) { PipelineLayoutDescriptor pipelineLayoutDescriptor = new() { @@ -140,7 +141,7 @@ private bool TryCreateCoveragePipelineLocked() BindGroupLayouts = null }; - this.coveragePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(this.device, in pipelineLayoutDescriptor); + this.coveragePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(device, in pipelineLayoutDescriptor); if (this.coveragePipelineLayout is null) { return false; @@ -166,7 +167,7 @@ private bool TryCreateCoveragePipelineLocked() NextInChain = (ChainedStruct*)&wgslDescriptor }; - shaderModule = this.webGPU.DeviceCreateShaderModule(this.device, in shaderDescriptor); + shaderModule = this.webGPU.DeviceCreateShaderModule(device, in shaderDescriptor); } if (shaderModule is null) @@ -270,7 +271,7 @@ private bool TryCreateCoveragePipelineLocked() Fragment = &stencilFragmentState }; - this.coverageStencilEvenOddPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in evenOddPipelineDescriptor); + this.coverageStencilEvenOddPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in evenOddPipelineDescriptor); if (this.coverageStencilEvenOddPipeline is null) { return false; @@ -308,7 +309,7 @@ private bool TryCreateCoveragePipelineLocked() Fragment = &stencilFragmentState }; - this.coverageStencilNonZeroIncrementPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in incrementPipelineDescriptor); + this.coverageStencilNonZeroIncrementPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in incrementPipelineDescriptor); if (this.coverageStencilNonZeroIncrementPipeline is null) { return false; @@ -346,7 +347,7 @@ private bool TryCreateCoveragePipelineLocked() Fragment = &stencilFragmentState }; - this.coverageStencilNonZeroDecrementPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in decrementPipelineDescriptor); + this.coverageStencilNonZeroDecrementPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in decrementPipelineDescriptor); if (this.coverageStencilNonZeroDecrementPipeline is null) { return false; @@ -425,7 +426,7 @@ private bool TryCreateCoveragePipelineLocked() Fragment = &coverFragmentState }; - this.coverageCoverPipeline = this.webGPU.DeviceCreateRenderPipeline(this.device, in coverPipelineDescriptor); + this.coverageCoverPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in coverPipelineDescriptor); } } @@ -441,6 +442,7 @@ private bool TryCreateCoveragePipelineLocked() } private bool TryEnsureCoverageScratchTargetsLocked( + Device* device, int width, int height, out TextureView* multisampleCoverageView, @@ -481,7 +483,7 @@ this.coverageScratchStencilView is not null && }; Texture* createdMultisampleCoverageTexture = - this.webGPU.DeviceCreateTexture(this.device, in multisampleCoverageTextureDescriptor); + this.webGPU.DeviceCreateTexture(device, in multisampleCoverageTextureDescriptor); if (createdMultisampleCoverageTexture is null) { return false; @@ -515,7 +517,7 @@ this.coverageScratchStencilView is not null && SampleCount = CoverageSampleCount }; - Texture* createdStencilTexture = this.webGPU.DeviceCreateTexture(this.device, in stencilTextureDescriptor); + Texture* createdStencilTexture = this.webGPU.DeviceCreateTexture(device, in stencilTextureDescriptor); if (createdStencilTexture is null) { this.ReleaseTextureViewLocked(createdMultisampleCoverageView); @@ -555,7 +557,7 @@ this.coverageScratchStencilView is not null && return true; } - private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) + private bool TryEnsureCoverageScratchVertexBufferLocked(Device* device, ulong requiredByteCount) { if (this.coverageScratchVertexBuffer is not null && this.coverageScratchVertexCapacityBytes >= requiredByteCount) @@ -573,7 +575,7 @@ private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) Size = requiredByteCount }; - WgpuBuffer* createdVertexBuffer = this.webGPU.DeviceCreateBuffer(this.device, in vertexBufferDescriptor); + WgpuBuffer* createdVertexBuffer = this.webGPU.DeviceCreateBuffer(device, in vertexBufferDescriptor); if (createdVertexBuffer is null) { return false; @@ -590,6 +592,8 @@ private bool TryEnsureCoverageScratchVertexBufferLocked(ulong requiredByteCount) private bool TryRasterizeCoverageTextureLocked( in CoverageTriangleData coverageTriangleData, in RasterizerOptions rasterizerOptions, + Device* device, + Queue* queue, out Texture* coverageTexture, out TextureView* coverageView) { @@ -605,6 +609,7 @@ private bool TryRasterizeCoverageTextureLocked( try { if (!this.TryEnsureCoverageScratchTargetsLocked( + device, rasterizerOptions.Interest.Width, rasterizerOptions.Interest.Height, out TextureView* multisampleCoverageView, @@ -623,7 +628,7 @@ private bool TryRasterizeCoverageTextureLocked( SampleCount = 1 }; - createdCoverageTexture = this.webGPU.DeviceCreateTexture(this.device, in coverageTextureDescriptor); + createdCoverageTexture = this.webGPU.DeviceCreateTexture(device, in coverageTextureDescriptor); if (createdCoverageTexture is null) { return false; @@ -647,18 +652,18 @@ private bool TryRasterizeCoverageTextureLocked( } ulong vertexByteCount = checked(coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); - if (!this.TryEnsureCoverageScratchVertexBufferLocked(vertexByteCount) || this.coverageScratchVertexBuffer is null) + if (!this.TryEnsureCoverageScratchVertexBufferLocked(device, vertexByteCount) || this.coverageScratchVertexBuffer is null) { return false; } fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) { - this.webGPU.QueueWriteBuffer(this.queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); + this.webGPU.QueueWriteBuffer(queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); } CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGPU.DeviceCreateCommandEncoder(this.device, in commandEncoderDescriptor); + commandEncoder = this.webGPU.DeviceCreateCommandEncoder(device, in commandEncoderDescriptor); if (commandEncoder is null) { return false; @@ -741,7 +746,7 @@ private bool TryRasterizeCoverageTextureLocked( return false; } - this.webGPU.QueueSubmit(this.queue, 1, ref commandBuffer); + this.webGPU.QueueSubmit(queue, 1, ref commandBuffer); this.webGPU.CommandBufferRelease(commandBuffer); commandBuffer = null; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index 7a4ed931c..55253504e 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -47,6 +47,16 @@ internal static unsafe class WebGPURuntime /// private static Wgpu? wgpuExtension; + /// + /// Shared device handle used by active backends in the current process. + /// + private static nint sharedDeviceHandle; + + /// + /// Shared queue handle used by active backends in the current process. + /// + private static nint sharedQueueHandle; + /// /// Number of currently active runtime leases. /// @@ -80,7 +90,7 @@ public static Lease Acquire() if (wgpuExtension is null) { - _ = api.TryGetDeviceExtension(null, out wgpuExtension); + api.TryGetDeviceExtension(null, out wgpuExtension); } leaseCount++; @@ -88,6 +98,55 @@ public static Lease Acquire() } } + /// + /// Sets shared GPU handles for active backend execution. + /// + /// Opaque device handle. + /// Opaque queue handle. + internal static void SetSharedHandles(nint deviceHandle, nint queueHandle) + { + lock (Sync) + { + sharedDeviceHandle = deviceHandle; + sharedQueueHandle = queueHandle; + } + } + + /// + /// Clears shared GPU handles. + /// + internal static void ClearSharedHandles() + { + lock (Sync) + { + sharedDeviceHandle = 0; + sharedQueueHandle = 0; + } + } + + /// + /// Attempts to get shared GPU handles. + /// + /// Receives the shared device pointer. + /// Receives the shared queue pointer. + /// when handles are available; otherwise . + internal static bool TryGetSharedHandles(out Device* device, out Queue* queue) + { + lock (Sync) + { + if (sharedDeviceHandle == 0 || sharedQueueHandle == 0) + { + device = null; + queue = null; + return false; + } + + device = (Device*)sharedDeviceHandle; + queue = (Queue*)sharedQueueHandle; + return true; + } + } + /// /// Releases one active runtime lease. /// @@ -154,6 +213,9 @@ private static void OnProcessExit(object? sender, EventArgs e) /// private static void DisposeRuntimeCore() { + sharedDeviceHandle = 0; + sharedQueueHandle = 0; + try { wgpuExtension?.Dispose(); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs index fe821cbd7..43729ff48 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; - namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// @@ -17,7 +15,7 @@ public sealed class WebGPUSurfaceCapability /// Opaque WGPUQueue* handle. /// Opaque WGPUTexture* handle for the current frame when writable upload is supported. /// Opaque WGPUTextureView* handle for the current frame. - /// Native render target texture format. + /// Native render target texture format identifier. /// Surface width in pixels. /// Surface height in pixels. /// Whether the target format is sRGB encoded. @@ -27,7 +25,7 @@ public WebGPUSurfaceCapability( nint queue, nint targetTexture, nint targetTextureView, - TextureFormat targetFormat, + WebGPUTextureFormatId targetFormat, int width, int height, bool isSrgb, @@ -65,9 +63,9 @@ public WebGPUSurfaceCapability( public nint TargetTextureView { get; } /// - /// Gets the native render target texture format. + /// Gets the native render target texture format identifier. /// - public TextureFormat TargetFormat { get; } + public WebGPUTextureFormatId TargetFormat { get; } /// /// Gets the surface width in pixels. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs new file mode 100644 index 000000000..2bc92c661 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Internal helper for benchmark/test-only native WebGPU target allocation. +/// +internal static unsafe class WebGPUTestNativeSurfaceAllocator +{ + internal static bool TryCreate( + WebGPUDrawingBackend backend, + int width, + int height, + bool isSrgb, + bool isPremultipliedAlpha, + out NativeSurface surface, + out nint textureHandle, + out nint textureViewHandle, + out string error) + where TPixel : unmanaged, IPixel + { + if (!backend.TryGetInteropHandles(out nint deviceHandle, out nint queueHandle)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = "WebGPU backend is not initialized."; + return false; + } + + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = $"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."; + return false; + } + + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + + // Lease.Dispose only decrements the runtime ref-count; it does not dispose the shared WebGPU API. + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + Device* device = (Device*)deviceHandle; + + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* targetTexture = api.DeviceCreateTexture(device, in targetTextureDescriptor); + if (targetTexture is null) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = "WebGPU.DeviceCreateTexture returned null."; + return false; + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* targetView = api.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + api.TextureRelease(targetTexture); + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = "WebGPU.TextureCreateView returned null."; + return false; + } + + textureHandle = (nint)targetTexture; + textureViewHandle = (nint)targetView; + surface = WebGPUNativeSurfaceFactory.Create( + deviceHandle, + queueHandle, + textureHandle, + textureViewHandle, + formatId, + width, + height, + isSrgb, + isPremultipliedAlpha); + error = string.Empty; + return true; + } + + internal static void Release(nint textureHandle, nint textureViewHandle) + { + if (textureHandle == 0 && textureViewHandle == 0) + { + return; + } + + // Keep the runtime alive while releasing native handles. + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + if (textureViewHandle != 0) + { + api.TextureViewRelease((TextureView*)textureViewHandle); + } + + if (textureHandle != 0) + { + api.TextureRelease((Texture*)textureHandle); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs new file mode 100644 index 000000000..278f4690e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Public WebGPU texture format identifiers used by . +/// Numeric values intentionally match WGPUTextureFormat. +/// +public enum WebGPUTextureFormatId +{ + /// + /// Single-channel 8-bit normalized unsigned format. + /// + R8Unorm = 0x01, + + /// + /// Two-channel 8-bit normalized unsigned format. + /// + RG8Unorm = 0x08, + + /// + /// Two-channel 8-bit normalized signed format. + /// + RG8Snorm = 0x09, + + /// + /// Four-channel 8-bit normalized signed format. + /// + Rgba8Snorm = 0x14, + + /// + /// Single-channel 16-bit floating-point format. + /// + R16Float = 0x07, + + /// + /// Two-channel 16-bit floating-point format. + /// + RG16Float = 0x11, + + /// + /// Four-channel 16-bit floating-point format. + /// + Rgba16Float = 0x22, + + /// + /// Two-channel 16-bit signed integer format. + /// + RG16Sint = 0x10, + + /// + /// Four-channel 16-bit signed integer format. + /// + Rgba16Sint = 0x21, + + /// + /// Packed 10:10:10:2 normalized unsigned format. + /// + Rgb10A2Unorm = 0x1A, + + /// + /// Four-channel 8-bit normalized unsigned RGBA format. + /// + Rgba8Unorm = 0x12, + + /// + /// Four-channel 8-bit normalized unsigned BGRA format. + /// + Bgra8Unorm = 0x17, + + /// + /// Four-channel 32-bit floating-point format. + /// + Rgba32Float = 0x23, + + /// + /// Single-channel 16-bit unsigned integer format. + /// + R16Uint = 0x05, + + /// + /// Two-channel 16-bit unsigned integer format. + /// + RG16Uint = 0x0F, + + /// + /// Four-channel 16-bit unsigned integer format. + /// + Rgba16Uint = 0x20, + + /// + /// Four-channel 8-bit unsigned integer format. + /// + Rgba8Uint = 0x15 +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs new file mode 100644 index 000000000..07def310e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class WebGPUTextureFormatMapper +{ + public static TextureFormat ToSilk(WebGPUTextureFormatId formatId) + => (TextureFormat)(int)formatId; + + public static WebGPUTextureFormatId FromSilk(TextureFormat textureFormat) + => (WebGPUTextureFormatId)(int)textureFormat; +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs index 7db484b26..217ad9a46 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs @@ -9,11 +9,15 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed class CompositionBatch { public CompositionBatch( - CompositionCoverageDefinition definition, - IReadOnlyList commands) + in CompositionCoverageDefinition definition, + IReadOnlyList commands, + int flushId = 0, + bool isFinalBatchInFlush = true) { this.Definition = definition; this.Commands = commands; + this.FlushId = flushId; + this.IsFinalBatchInFlush = isFinalBatchInFlush; } /// @@ -25,4 +29,14 @@ public CompositionBatch( /// Gets normalized composition commands in original draw order. /// public IReadOnlyList Commands { get; } + + /// + /// Gets the batcher flush identifier shared by all batches emitted from one canvas flush call. + /// + public int FlushId { get; } + + /// + /// Gets a value indicating whether this is the last batch emitted for the current flush identifier. + /// + public bool IsFinalBatchInFlush { get; } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index 1024ceb14..def345965 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -26,7 +26,7 @@ private CompositionCommand( Brush brush, Rectangle brushBounds, GraphicsOptions graphicsOptions, - RasterizerOptions rasterizerOptions, + in RasterizerOptions rasterizerOptions, Point destinationOffset) { this.DefinitionKey = definitionKey; @@ -89,7 +89,7 @@ public static CompositionCommand Create( in RasterizerOptions rasterizerOptions, Point destinationOffset = default) { - int definitionKey = ComputeCoverageDefinitionKey(path, rasterizerOptions); + int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions); RectangleF bounds = path.Bounds; Rectangle localBrushBounds = Rectangle.FromLTRB( (int)MathF.Floor(bounds.Left), @@ -108,7 +108,7 @@ public static CompositionCommand Create( brush, brushBounds, graphicsOptions, - rasterizerOptions, + in rasterizerOptions, destinationOffset); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 42d5502f9..ee4cdc780 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -131,10 +131,22 @@ private Buffer2D GetOrCreateCoverageMap( in CompositionCoverageDefinition definition, MemoryAllocator allocator) { - CompositionCoverageDefinition localDefinition = definition; - return this.coverageCache.GetOrAdd( - localDefinition.DefinitionKey, - _ => this.CreateCoverageMap(localDefinition, allocator)); + // Hot path: coverage for this definition is already cached. + if (this.coverageCache.TryGetValue(definition.DefinitionKey, out Buffer2D? cached)) + { + return cached; + } + + // Miss path: create coverage once for this definition. + Buffer2D created = this.CreateCoverageMap(definition, allocator); + if (this.coverageCache.TryAdd(definition.DefinitionKey, created)) + { + return created; + } + + // Another thread won the insert race; dispose loser map and use the winner. + created.Dispose(); + return this.coverageCache[definition.DefinitionKey]; } private Buffer2D CreateCoverageMap( diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index e9ba7485c..0d880c80d 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -17,6 +17,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; internal sealed class DrawingCanvasBatcher where TPixel : unmanaged, IPixel { + private static int nextFlushId; private readonly Configuration configuration; private readonly IDrawingBackend backend; private readonly ICanvasFrame targetFrame; @@ -67,6 +68,7 @@ public void FlushCompositions() { Rectangle targetBounds = this.targetFrame.Bounds; int index = 0; + List batches = []; while (index < this.commands.Count) { CompositionCommand definitionCommand = this.commands[index]; @@ -123,10 +125,28 @@ public void FlushCompositions() definitionCommand.Path, definitionCommand.RasterizerOptions); + batches.Add(new CompositionBatch(definition, preparedCommands)); + } + + if (batches.Count == 0) + { + return; + } + + // All batches emitted by this call share one flush id so backends can keep + // transient per-flush GPU state and finalize once on the last batch. + int flushId = Interlocked.Increment(ref nextFlushId); + for (int i = 0; i < batches.Count; i++) + { + CompositionBatch batch = batches[i]; this.backend.FlushCompositions( this.configuration, this.targetFrame, - new CompositionBatch(definition, preparedCommands)); + new CompositionBatch( + batch.Definition, + batch.Commands, + flushId, + isFinalBatchInFlush: i == batches.Count - 1)); } } finally diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index 25a60264e..45b403fcb 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -81,10 +81,7 @@ public RichTextGlyphRenderer( public List DrawingOperations { get; } /// - protected override void BeginText(in FontRectangle bounds) - { - this.DrawingOperations.Clear(); - } + protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear(); /// protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) @@ -126,6 +123,7 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara parameters, new RectangleF(subPixelLocation, subPixelSize), this.currentPen ?? this.defaultPen); + if (this.glyphCache.ContainsKey(this.currentCacheKey)) { // We have already drawn the glyph vectors. diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index 9d3a0407f..dc944d68a 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -81,16 +81,16 @@ public override void Apply(Span scanline, int x, int y) { int localY = y - this.TargetRegion.Rectangle.Y; int localX = x - this.TargetRegion.Rectangle.X; - Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY).Slice(localX); + Span destinationRow = this.TargetRegion.DangerousGetRowSpan(localY)[localX..]; // Constrain the spans to each other if (destinationRow.Length > scanline.Length) { - destinationRow = destinationRow.Slice(0, scanline.Length); + destinationRow = destinationRow[..scanline.Length]; } else { - scanline = scanline.Slice(0, destinationRow.Length); + scanline = scanline[..destinationRow.Length]; } Configuration configuration = this.Configuration; @@ -101,7 +101,7 @@ public override void Apply(Span scanline, int x, int y) } else { - Span amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length); + Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; for (int i = 0; i < scanline.Length; i++) { diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 3981cb8cd..4b6ee94b9 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -66,17 +66,19 @@ public void Setup() this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); this.webGpuCpuImage = new Image(this.webGpuConfiguration, Width, Height); - if (!this.webGpuBackend.TryCreateNativeSurfaceTarget( + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + this.webGpuBackend, Width, Height, isSrgb: false, isPremultipliedAlpha: false, out NativeSurface nativeSurface, out this.webGpuNativeTextureHandle, - out this.webGpuNativeTextureViewHandle)) + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) { throw new InvalidOperationException( - $"Unable to create benchmark native WebGPU target. GPUReady={this.webGpuBackend.IsGPUReady}, Error='{this.webGpuBackend.LastGPUInitializationFailure ?? ""}'."); + $"Unable to create benchmark native WebGPU target. GPUReady={this.webGpuBackend.TestingIsGPUReady}, Error='{(nativeSurfaceError.Length > 0 ? nativeSurfaceError : this.webGpuBackend.TestingLastGPUInitializationFailure ?? "")}'."); } this.webGpuNativeFrame = new NativeSurfaceOnlyFrame( @@ -109,7 +111,9 @@ public void Cleanup() { this.defaultImage.Dispose(); this.webGpuCpuImage.Dispose(); - this.webGpuBackend.ReleaseNativeSurfaceTarget(this.webGpuNativeTextureHandle, this.webGpuNativeTextureViewHandle); + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); this.webGpuNativeTextureHandle = 0; this.webGpuNativeTextureViewHandle = 0; this.webGpuBackend.Dispose(); diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index c082ef741..b127c7141 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -31,6 +31,7 @@ + @@ -56,4 +57,3 @@ - diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index e9cfc15ed..10a4764ba 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -2,10 +2,8 @@ // Licensed under the Six Labors Split License. using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -35,14 +33,14 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImagePro webGpuImage.Configuration.SetDrawingBackend(backend); webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - Assert.True(backend.PrepareCoverageCallCount > 0); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); + Assert.True(backend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); + Assert.Equal(0, backend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(backend); - if (backend.IsGPUReady) + if (backend.TestingIsGPUReady) { - Assert.True(backend.GPUPrepareCoverageCallCount > 0); - Assert.True(backend.GPUCompositeCoverageCallCount + backend.FallbackCompositeCoverageCallCount > 0); + Assert.True(backend.TestingGPUPrepareCoverageCallCount > 0); + Assert.True(backend.TestingGPUCompositeCoverageCallCount + backend.TestingFallbackCompositeCoverageCallCount > 0); } ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); @@ -96,9 +94,9 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImagePro webGpuImage.Configuration.SetDrawingBackend(backend); webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); - Assert.True(backend.PrepareCoverageCallCount > 0); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); + Assert.True(backend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); + Assert.Equal(0, backend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(backend); AssertGpuPathWhenRequired(backend); @@ -148,10 +146,10 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - Assert.True(backend.PrepareCoverageCallCount > 0); - Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); + Assert.True(backend.TestingPrepareCoverageCallCount > 0); + Assert.True(backend.TestingCompositeCoverageCallCount >= backend.TestingPrepareCoverageCallCount); + Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); + Assert.Equal(0, backend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(backend); AssertGpuPathWhenRequired(backend); @@ -190,10 +188,10 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider= backend.PrepareCoverageCallCount); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); + Assert.InRange(backend.TestingPrepareCoverageCallCount, 1, 20); + Assert.True(backend.TestingCompositeCoverageCallCount >= backend.TestingPrepareCoverageCallCount); + Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); + Assert.Equal(0, backend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(backend); AssertGpuPathWhenRequired(backend); } @@ -201,11 +199,11 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider 0, - $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.PrepareCoverageCallCount}/{backend.GPUPrepareCoverageCallCount}/{backend.FallbackPrepareCoverageCallCount}"); + backend.TestingGPUPrepareCoverageCallCount > 0, + $"No GPU coverage preparation calls were observed. Prepare(total/gpu/fallback)={backend.TestingPrepareCoverageCallCount}/{backend.TestingGPUPrepareCoverageCallCount}/{backend.TestingFallbackPrepareCoverageCallCount}"); Assert.True( - backend.GPUCompositeCoverageCallCount > 0, - $"No GPU composite calls were observed. Composite(total/gpu/fallback)={backend.CompositeCoverageCallCount}/{backend.GPUCompositeCoverageCallCount}/{backend.FallbackCompositeCoverageCallCount}"); + backend.TestingGPUCompositeCoverageCallCount > 0, + $"No GPU composite calls were observed. Composite(total/gpu/fallback)={backend.TestingCompositeCoverageCallCount}/{backend.TestingGPUCompositeCoverageCallCount}/{backend.TestingFallbackCompositeCoverageCallCount}"); Assert.Equal( 0, - backend.FallbackPrepareCoverageCallCount); + backend.TestingFallbackPrepareCoverageCallCount); Assert.Equal( 0, - backend.FallbackCompositeCoverageCallCount); + backend.TestingFallbackCompositeCoverageCallCount); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs new file mode 100644 index 000000000..324321d72 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUTextureFormatMapperTests +{ + [Fact] + public void Mapper_UsesExactSilkEnumValues_ForAllSupportedFormats() + { + (WebGPUTextureFormatId Drawing, TextureFormat Silk)[] mappings = + [ + (WebGPUTextureFormatId.R8Unorm, TextureFormat.R8Unorm), + (WebGPUTextureFormatId.RG8Unorm, TextureFormat.RG8Unorm), + (WebGPUTextureFormatId.RG8Snorm, TextureFormat.RG8Snorm), + (WebGPUTextureFormatId.Rgba8Snorm, TextureFormat.Rgba8Snorm), + (WebGPUTextureFormatId.R16Float, TextureFormat.R16float), + (WebGPUTextureFormatId.RG16Float, TextureFormat.RG16float), + (WebGPUTextureFormatId.Rgba16Float, TextureFormat.Rgba16float), + (WebGPUTextureFormatId.RG16Sint, TextureFormat.RG16Sint), + (WebGPUTextureFormatId.Rgba16Sint, TextureFormat.Rgba16Sint), + (WebGPUTextureFormatId.Rgb10A2Unorm, TextureFormat.Rgb10A2Unorm), + (WebGPUTextureFormatId.Rgba8Unorm, TextureFormat.Rgba8Unorm), + (WebGPUTextureFormatId.Bgra8Unorm, TextureFormat.Bgra8Unorm), + (WebGPUTextureFormatId.Rgba32Float, TextureFormat.Rgba32float), + (WebGPUTextureFormatId.R16Uint, TextureFormat.R16Uint), + (WebGPUTextureFormatId.RG16Uint, TextureFormat.RG16Uint), + (WebGPUTextureFormatId.Rgba16Uint, TextureFormat.Rgba16Uint), + (WebGPUTextureFormatId.Rgba8Uint, TextureFormat.Rgba8Uint) + ]; + + Assert.Equal(Enum.GetValues().Length, mappings.Length); + + foreach ((WebGPUTextureFormatId drawing, TextureFormat silk) in mappings) + { + Assert.Equal((int)silk, (int)drawing); + Assert.Equal(silk, WebGPUTextureFormatMapper.ToSilk(drawing)); + Assert.Equal(drawing, WebGPUTextureFormatMapper.FromSilk(silk)); + } + } +} From 079488ae5f0122958cc281a1acbeaac4f79b4704 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 16:43:19 +1000 Subject: [PATCH 010/136] Improve WebGPU readback, batching and coverage --- .../WebGPUDrawingBackend.cs | 19 +- .../WebGPUFlushContext.cs | 3 +- .../WebGPUTestNativeSurfaceAllocator.cs | 195 +++++++++++++ .../Backends/DefaultDrawingBackend.cs | 34 +-- .../DrawingCanvasBatcher{TPixel}.cs | 8 +- .../Processing/DrawingCanvas{TPixel}.cs | 31 +- .../Backends/WebGPUDrawingBackendTests.cs | 264 +++++++++++++++++- 7 files changed, 510 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index cd390cb72..c8821db35 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -190,9 +190,18 @@ public void FlushCompositions( { gpuReady = true; gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); - if (gpuSuccess && (!useCpuReadbackFlushSession || compositionBatch.IsFinalBatchInFlush)) + if (gpuSuccess) { - gpuSuccess = this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion); + if (useCpuReadbackFlushSession) + { + gpuSuccess = compositionBatch.IsFinalBatchInFlush + ? this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion) + : TrySubmitBatch(flushContext); + } + else + { + gpuSuccess = this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion); + } } } } @@ -441,6 +450,12 @@ private static bool TrySubmit(WebGPUFlushContext flushContext) } } + private static bool TrySubmitBatch(WebGPUFlushContext flushContext) + { + flushContext.EndRenderPassIfOpen(); + return TrySubmit(flushContext); + } + private bool TryReadBackToCpuRegion(WebGPUFlushContext flushContext, Buffer2DRegion destinationRegion) where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 8778c90ca..231a5a822 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -104,6 +104,7 @@ public static WebGPUFlushContext Create( device = (Device*)nativeCapability.Device; queue = (Queue*)nativeCapability.Queue; textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); + bounds = new Rectangle(0, 0, nativeCapability.Width, nativeCapability.Height); deviceState = GetOrCreateDeviceState(lease.Api, device); context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, deviceState); context.InitializeNativeTarget(nativeCapability); @@ -142,7 +143,7 @@ public static WebGPUFlushContext CreateUploadContext(ICanvasFrame internal static unsafe class WebGPUTestNativeSurfaceAllocator { + private const int CallbackTimeoutMilliseconds = 5000; + internal static bool TryCreate( WebGPUDrawingBackend backend, int width, @@ -106,6 +113,173 @@ internal static bool TryCreate( return true; } + internal static bool TryReadTexture( + WebGPUDrawingBackend backend, + nint textureHandle, + int width, + int height, + out Image? image, + out string error) + where TPixel : unmanaged, IPixel + { + image = null; + if (textureHandle == 0) + { + error = "Texture handle is zero."; + return false; + } + + if (width <= 0 || height <= 0) + { + error = "Texture dimensions must be greater than zero."; + return false; + } + + if (!backend.TryGetInteropHandles(out nint deviceHandle, out nint queueHandle)) + { + error = backend.TestingLastGPUInitializationFailure ?? "WebGPU backend is not initialized."; + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + Device* device = (Device*)deviceHandle; + Queue* queue = (Queue*)queueHandle; + + int pixelSizeInBytes = Unsafe.SizeOf(); + int packedRowBytes = checked(width * pixelSizeInBytes); + int readbackRowBytes = Align(packedRowBytes, 256); + int packedByteCount = checked(packedRowBytes * height); + ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)height); + + Silk.NET.WebGPU.Buffer* readbackBuffer = null; + CommandEncoder* commandEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor bufferDescriptor = new() + { + Usage = BufferUsage.CopyDst | BufferUsage.MapRead, + Size = readbackByteCount, + MappedAtCreation = false + }; + + readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor); + if (readbackBuffer is null) + { + error = "WebGPU.DeviceCreateBuffer returned null for readback."; + return false; + } + + CommandEncoderDescriptor encoderDescriptor = default; + commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); + if (commandEncoder is null) + { + error = "WebGPU.DeviceCreateCommandEncoder returned null."; + return false; + } + + ImageCopyTexture source = new() + { + Texture = (Texture*)textureHandle, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + ImageCopyBuffer destination = new() + { + Buffer = readbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = (uint)readbackRowBytes, + RowsPerImage = (uint)height + } + }; + + Extent3D copySize = new((uint)width, (uint)height, 1); + api.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + error = "WebGPU.CommandEncoderFinish returned null."; + return false; + } + + api.QueueSubmit(queue, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + api.CommandEncoderRelease(commandEncoder); + commandEncoder = null; + + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim mapReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userData) + { + _ = userData; + mapStatus = status; + mapReady.Set(); + } + + using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null); + if (!WaitForSignal(lease.WgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + { + error = $"WebGPU readback map failed with status '{mapStatus}'."; + return false; + } + + void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount); + if (mapped is null) + { + api.BufferUnmap(readbackBuffer); + error = "WebGPU.BufferGetConstMappedRange returned null."; + return false; + } + + try + { + ReadOnlySpan readback = new(mapped, checked((int)readbackByteCount)); + byte[] packed = new byte[packedByteCount]; + Span packedSpan = packed; + for (int y = 0; y < height; y++) + { + readback + .Slice(y * readbackRowBytes, packedRowBytes) + .CopyTo(packedSpan.Slice(y * packedRowBytes, packedRowBytes)); + } + + image = Image.LoadPixelData(packed, width, height); + error = string.Empty; + return true; + } + finally + { + api.BufferUnmap(readbackBuffer); + } + } + finally + { + if (commandBuffer is not null) + { + api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + api.CommandEncoderRelease(commandEncoder); + } + + if (readbackBuffer is not null) + { + api.BufferRelease(readbackBuffer); + } + } + } + internal static void Release(nint textureHandle, nint textureViewHandle) { if (textureHandle == 0 && textureViewHandle == 0) @@ -126,4 +300,25 @@ internal static void Release(nint textureHandle, nint textureViewHandle) api.TextureRelease((Texture*)textureHandle); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Align(int value, int alignment) + => ((value + alignment - 1) / alignment) * alignment; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WaitForSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + { + if (extension is null) + { + return signal.Wait(CallbackTimeoutMilliseconds); + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) + { + _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + } + + return signal.IsSet; + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index ee4cdc780..2ffc3ef26 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Collections.Concurrent; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -12,8 +11,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed class DefaultDrawingBackend : IDrawingBackend { - private readonly ConcurrentDictionary> coverageCache = new(); - /// /// Initializes a new instance of the class. /// @@ -72,7 +69,7 @@ public void FlushCompositions( _ = target.TryGetCpuRegion(out Buffer2DRegion destinationFrame); CompositionCoverageDefinition definition = compositionBatch.Definition; - Buffer2D coverageMap = this.GetOrCreateCoverageMap(definition, configuration.MemoryAllocator); + using Buffer2D coverageMap = this.CreateCoverageMap(definition, configuration.MemoryAllocator); Rectangle destinationBounds = destinationFrame.Rectangle; IReadOnlyList commands = compositionBatch.Commands; @@ -127,28 +124,6 @@ public void FlushCompositions( } } - private Buffer2D GetOrCreateCoverageMap( - in CompositionCoverageDefinition definition, - MemoryAllocator allocator) - { - // Hot path: coverage for this definition is already cached. - if (this.coverageCache.TryGetValue(definition.DefinitionKey, out Buffer2D? cached)) - { - return cached; - } - - // Miss path: create coverage once for this definition. - Buffer2D created = this.CreateCoverageMap(definition, allocator); - if (this.coverageCache.TryAdd(definition.DefinitionKey, created)) - { - return created; - } - - // Another thread won the insert race; dispose loser map and use the winner. - created.Dispose(); - return this.coverageCache[definition.DefinitionKey]; - } - private Buffer2D CreateCoverageMap( in CompositionCoverageDefinition definition, MemoryAllocator allocator) @@ -173,11 +148,6 @@ private Buffer2D CreateCoverageMap( public void Dispose() { - foreach (Buffer2D entry in this.coverageCache.Values) - { - entry.Dispose(); - } - - this.coverageCache.Clear(); + GC.KeepAlive(this.PrimaryRasterizer); } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 0d880c80d..2567b4f66 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -76,9 +77,14 @@ public void FlushCompositions() // Build one batch for the contiguous run sharing the same coverage definition. List preparedCommands = []; - for (; index < this.commands.Count && this.commands[index].DefinitionKey == definitionKey; index++) + for (; index < this.commands.Count; index++) { CompositionCommand command = this.commands[index]; + if (command.DefinitionKey != definitionKey) + { + break; + } + Rectangle interest = command.RasterizerOptions.Interest; Rectangle commandDestination = new( command.DestinationOffset.X + interest.X, diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 019f03bf7..e2e05ad29 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -48,16 +48,30 @@ internal DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame) + : this( + configuration, + backend, + targetFrame, + new DrawingCanvasBatcher(configuration, backend, targetFrame)) + { + } + + private DrawingCanvas( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame, + DrawingCanvasBatcher batcher) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); Guard.NotNull(targetFrame, nameof(targetFrame)); + Guard.NotNull(batcher, nameof(batcher)); this.configuration = configuration; this.backend = backend; this.targetFrame = targetFrame; + this.batcher = batcher; this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); - this.batcher = new DrawingCanvasBatcher(configuration, backend, targetFrame); } /// @@ -76,7 +90,7 @@ public DrawingCanvas CreateRegion(Rectangle region) Rectangle clipped = Rectangle.Intersect(this.Bounds, region); ICanvasFrame childFrame = new CanvasRegionFrame(this.targetFrame, clipped); - return new DrawingCanvas(this.configuration, this.backend, childFrame); + return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher); } /// @@ -301,10 +315,19 @@ private CompositionCommand CreateCompositionCommand( IPath compositionPath; RasterizerSamplingOrigin samplingOrigin; + IntersectionRule intersectionRule = operation.IntersectionRule; if (operation.Kind == DrawingOperationKind.Draw) { - compositionPath = operation.Pen!.GeneratePath(operation.Path); + Pen pen = operation.Pen!; + compositionPath = pen.GeneratePath(operation.Path); samplingOrigin = RasterizerSamplingOrigin.PixelCenter; + + // Keep draw semantics aligned with DrawPath: non-normalized stroke output + // requires non-zero winding to preserve stroke interior behavior. + if (!pen.StrokeOptions.NormalizeOutput && intersectionRule != IntersectionRule.NonZero) + { + intersectionRule = IntersectionRule.NonZero; + } } else { @@ -330,7 +353,7 @@ private CompositionCommand CreateCompositionCommand( RasterizerOptions rasterizerOptions = new( interest, - operation.IntersectionRule, + intersectionRule, rasterizationMode, samplingOrigin); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 10a4764ba..dde036b6b 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -157,6 +158,189 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag comparer.VerifySimilarity(defaultImage, webGpuImage); } + [Theory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput(TestImageProvider provider) + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); + Brush brush = Brushes.Solid(Color.Black); + Brush clearBrush = Brushes.Solid(Color.White); + + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + { + defaultCanvas.Fill(clearBrush, clearOptions); + defaultCanvas.FillPath(polygon, brush, drawingOptions); + defaultCanvas.Flush(); + } + + defaultImage.DebugSave( + provider, + "DefaultBackend_FillPath_NativeSurfaceParity", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using WebGPUDrawingBackend backend = new(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + backend, + defaultImage.Width, + defaultImage.Height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using DrawingCanvas canvas = + new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); + canvas.Fill(clearBrush, clearOptions); + canvas.FillPath(polygon, brush, drawingOptions); + canvas.Flush(); + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + backend, + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image webGpuImage, + out string readError), + readError); + + using (webGpuImage) + { + webGpuImage.DebugSave( + provider, + "WebGPUBackend_FillPath_NativeSurfaceParity", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + [Theory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput(TestImageProvider provider) + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + Rectangle region = new(72, 64, 320, 240); + RectangularPolygon localPolygon = new(16.25F, 24.5F, 250.5F, 160.75F); + Brush brush = Brushes.Solid(Color.Black); + Brush clearBrush = Brushes.Solid(Color.White); + + using Image defaultImage = provider.GetImage(); + using DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage)); + defaultCanvas.Fill(clearBrush, clearOptions); + + using (DrawingCanvas defaultRegionCanvas = defaultCanvas.CreateRegion(region)) + { + defaultRegionCanvas.FillPath(localPolygon, brush, drawingOptions); + } + + defaultImage.DebugSave( + provider, + "DefaultBackend_FillPath_NativeSurfaceSubregionParity", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using WebGPUDrawingBackend backend = new(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + backend, + defaultImage.Width, + defaultImage.Height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using DrawingCanvas canvas = + new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); + canvas.Fill(clearBrush, clearOptions); + using (DrawingCanvas regionCanvas = canvas.CreateRegion(region)) + { + regionCanvas.FillPath(localPolygon, brush, drawingOptions); + } + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + backend, + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image webGpuImage, + out string readError), + readError); + + using (webGpuImage) + { + webGpuImage.DebugSave( + provider, + "WebGPUBackend_FillPath_NativeSurfaceSubregionParity", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + int defaultCoveragePixels = CountNonBackgroundPixels(defaultImage, Color.White); + int webGpuCoveragePixels = CountNonBackgroundPixels(webGpuImage, Color.White); + Assert.True(defaultCoveragePixels > 0, "Default backend produced no subregion fill coverage."); + Assert.True(webGpuCoveragePixels > 0, "WebGPU backend produced no subregion fill coverage."); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + [Theory] [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) @@ -176,18 +360,36 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider image = provider.GetImage(); + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); + defaultImage.DebugSave( + provider, + "DefaultBackend_RepeatedGlyphs", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = provider.GetImage(); using WebGPUDrawingBackend backend = new(); - image.Configuration.SetDrawingBackend(backend); + webGpuImage.Configuration.SetDrawingBackend(backend); - image.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); + webGpuImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); - image.DebugSave( + webGpuImage.DebugSave( provider, "WebGPUBackend_RepeatedGlyphs", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + int defaultCoveragePixels = CountNonBackgroundPixels(defaultImage, Color.White); + int webGpuCoveragePixels = CountNonBackgroundPixels(webGpuImage, Color.White); + Assert.True(defaultCoveragePixels > 0, "Default backend produced no text coverage."); + Assert.True( + webGpuCoveragePixels >= (defaultCoveragePixels * 9) / 10, + $"WebGPU text coverage is too low. default={defaultCoveragePixels}, webgpu={webGpuCoveragePixels}"); + + ImageComparer comparer = ImageComparer.TolerantPercentage(2F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + Assert.InRange(backend.TestingPrepareCoverageCallCount, 1, 20); Assert.True(backend.TestingCompositeCoverageCallCount >= backend.TestingPrepareCoverageCallCount); Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); @@ -234,4 +436,58 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) 0, backend.TestingFallbackCompositeCoverageCallCount); } + + private static int CountNonBackgroundPixels(Image image, Color background) + { + Rgba32 bg = background.ToPixel(); + Buffer2D buffer = image.Frames.RootFrame.PixelBuffer; + int count = 0; + for (int y = 0; y < buffer.Height; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 pixel = row[x]; + if (Math.Abs(pixel.R - bg.R) > 2 || + Math.Abs(pixel.G - bg.G) > 2 || + Math.Abs(pixel.B - bg.B) > 2 || + Math.Abs(pixel.A - bg.A) > 2) + { + count++; + } + } + } + + return count; + } + + private static Buffer2DRegion GetFrameRegion(Image image) + => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + private sealed class NativeSurfaceOnlyFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly Rectangle bounds; + private readonly NativeSurface surface; + + public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) + { + this.bounds = bounds; + this.surface = surface; + } + + public Rectangle Bounds => this.bounds; + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + public bool TryGetNativeSurface(out NativeSurface surface) + { + surface = this.surface; + return true; + } + } } From c07f6851a802325fc312049eed41ada8e250e032 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 18:35:40 +1000 Subject: [PATCH 011/136] Refactor WebGPU composite pipeline & instance data --- .../WebGPUCompositeInstanceData.cs | 29 ++ .../WebGPUDrawingBackend.cs | 309 +++++++----------- .../WebGPUFlushContext.cs | 108 +++--- .../Processing/DrawingCanvas{TPixel}.cs | 5 + 4 files changed, 210 insertions(+), 241 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs new file mode 100644 index 000000000..faac7974c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +[StructLayout(LayoutKind.Sequential)] +internal struct WebGPUCompositeInstanceData +{ + public uint SourceOffsetX; + public uint SourceOffsetY; + public uint DestinationX; + public uint DestinationY; + public uint DestinationWidth; + public uint DestinationHeight; + public uint TargetWidth; + public uint TargetHeight; + public uint BrushKind; + public uint Padding0; + public uint Padding1; + public uint Padding2; + public Vector4 SolidBrushColor; + public float BlendPercentage; + public float Padding3; + public float Padding4; + public float Padding5; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index c8821db35..2dae5c21a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1,9 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Diagnostics; -using System.Numerics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; @@ -143,16 +142,17 @@ public void FlushCompositions( this.TestingReleaseCoverageCallCount++; this.TestingCompositeCoverageCallCount += commandCount; + bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); + bool hasNativeSurface = target.TryGetNativeSurface(out _); + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) { this.TestingFallbackPrepareCoverageCallCount++; this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch); + this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); return; } - bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); - bool hasNativeSurface = target.TryGetNativeSurface(out _); bool useCpuReadbackFlushSession = hasCpuRegion && !hasNativeSurface && compositionBatch.FlushId != 0; bool gpuSuccess = false; bool gpuReady = false; @@ -171,37 +171,28 @@ public void FlushCompositions( out hadExistingCpuSession) : WebGPUFlushContext.Create(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); - lock (flushContext.DeviceState.SyncRoot) + CompositionCoverageDefinition definition = compositionBatch.Definition; + if (TryPrepareGpuResources( + flushContext, + in definition, + out RenderPipeline* pipeline, + out WebGPUFlushContext.CoverageEntry? coverageEntry, + out failure) && + coverageEntry is not null) { - CompositionCoverageDefinition definition = compositionBatch.Definition; - if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline(flushContext.TextureFormat, out RenderPipeline* pipeline, out failure)) - { - gpuSuccess = false; - } - else if (!flushContext.DeviceState.TryGetOrCreateCoverageEntry( - in definition, - flushContext.Queue, - out WebGPUFlushContext.CoverageEntry? coverageEntry, - out failure) || coverageEntry is null) - { - gpuSuccess = false; - } - else + gpuReady = true; + gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); + if (gpuSuccess) { - gpuReady = true; - gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); - if (gpuSuccess) + if (useCpuReadbackFlushSession) { - if (useCpuReadbackFlushSession) - { - gpuSuccess = compositionBatch.IsFinalBatchInFlush - ? this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion) - : TrySubmitBatch(flushContext); - } - else - { - gpuSuccess = this.TryFinalizeFlush(flushContext, hasCpuRegion, cpuRegion); - } + gpuSuccess = compositionBatch.IsFinalBatchInFlush + ? this.TryFinalizeFlush(flushContext, cpuRegion) + : TrySubmitBatch(flushContext); + } + else + { + gpuSuccess = this.TryFinalizeFlush(flushContext, cpuRegion); } } } @@ -211,6 +202,13 @@ public void FlushCompositions( failure = ex.Message; gpuSuccess = false; } + finally + { + if (!useCpuReadbackFlushSession) + { + flushContext?.Dispose(); + } + } this.TestingGPUInitializationAttempted = true; this.TestingIsGPUReady = gpuReady; @@ -239,11 +237,10 @@ public void FlushCompositions( this.TestingFallbackPrepareCoverageCallCount++; this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch); + this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); return; } - flushContext?.Dispose(); if (gpuSuccess) { this.TestingGPUPrepareCoverageCallCount++; @@ -253,16 +250,17 @@ public void FlushCompositions( this.TestingFallbackPrepareCoverageCallCount++; this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch); + this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); } private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch) + CompositionBatch compositionBatch, + bool hasCpuRegion) where TPixel : unmanaged, IPixel { - if (target.TryGetCpuRegion(out _)) + if (hasCpuRegion) { this.fallbackBackend.FlushCompositions(configuration, target, compositionBatch); return; @@ -277,16 +275,37 @@ private void FlushCompositionsFallback( this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionBatch); using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target); - lock (uploadContext.DeviceState.SyncRoot) + WebGPUFlushContext.UploadTextureFromRegion( + uploadContext.Api, + uploadContext.Queue, + uploadContext.TargetTexture, + stagingRegion); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryPrepareGpuResources( + WebGPUFlushContext flushContext, + in CompositionCoverageDefinition definition, + out RenderPipeline* pipeline, + [NotNullWhen(true)] out WebGPUFlushContext.CoverageEntry? coverageEntry, + out string? error) + { + lock (flushContext.DeviceState.SyncRoot) { - if (!this.QueueWriteTextureFromRegion( - uploadContext.Api, - uploadContext.Queue, - uploadContext.TargetTexture, - stagingRegion)) + if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline( + flushContext.TextureFormat, + out pipeline, + out error)) { - throw new NotSupportedException("Fallback upload to native WebGPU target failed."); + coverageEntry = null; + return false; } + + return flushContext.DeviceState.TryGetOrCreateCoverageEntry( + in definition, + flushContext.Queue, + out coverageEntry, + out error); } } @@ -303,7 +322,7 @@ private bool TryCompositeBatch( return true; } - nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); + nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); if (!flushContext.EnsureInstanceBufferCapacity(instanceBytes, CompositeInstanceBufferSize) || !flushContext.EnsureCommandEncoder() || !flushContext.BeginRenderPass()) @@ -311,61 +330,54 @@ private bool TryCompositeBatch( return false; } - CompositeInstanceData[] rented = ArrayPool.Shared.Rent(commandCount); - try + Span instances = flushContext.GetCompositeInstanceSpan(commandCount); + int targetWidth = flushContext.TargetBounds.Width; + int targetHeight = flushContext.TargetBounds.Height; + for (int i = 0; i < commandCount; i++) { - Span instances = rented.AsSpan(0, commandCount); - int targetWidth = flushContext.TargetBounds.Width; - int targetHeight = flushContext.TargetBounds.Height; - for (int i = 0; i < commandCount; i++) + PreparedCompositionCommand command = commands[i]; + if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) { - PreparedCompositionCommand command = commands[i]; - if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) - { - return false; - } - - int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; - int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; - - instances[i] = new CompositeInstanceData - { - SourceOffsetX = (uint)command.SourceOffset.X, - SourceOffsetY = (uint)command.SourceOffset.Y, - DestinationX = (uint)destinationX, - DestinationY = (uint)destinationY, - DestinationWidth = (uint)command.DestinationRegion.Width, - DestinationHeight = (uint)command.DestinationRegion.Height, - TargetWidth = (uint)targetWidth, - TargetHeight = (uint)targetHeight, - BrushKind = (uint)brushData.Kind, - SolidBrushColor = brushData.SolidColor, - BlendPercentage = command.GraphicsOptions.BlendPercentage - }; + return false; } - fixed (CompositeInstanceData* instancesPtr = instances) - { - flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, 0, instancesPtr, instanceBytes); - } + int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; + int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; - BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceBytes); - if (bindGroup is null) + instances[i] = new WebGPUCompositeInstanceData { - return false; - } - - flushContext.TrackBindGroup(bindGroup); - flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); - flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); - flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, (uint)commandCount, 0, 0); + SourceOffsetX = (uint)command.SourceOffset.X, + SourceOffsetY = (uint)command.SourceOffset.Y, + DestinationX = (uint)destinationX, + DestinationY = (uint)destinationY, + DestinationWidth = (uint)command.DestinationRegion.Width, + DestinationHeight = (uint)command.DestinationRegion.Height, + TargetWidth = (uint)targetWidth, + TargetHeight = (uint)targetHeight, + BrushKind = (uint)brushData.Kind, + SolidBrushColor = brushData.SolidColor, + BlendPercentage = command.GraphicsOptions.BlendPercentage + }; + } - return true; + fixed (WebGPUCompositeInstanceData* instancesPtr = instances) + { + // QueueWriteBuffer copies source bytes into driver-owned staging immediately. + flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, 0, instancesPtr, instanceBytes); } - finally + + BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceBytes); + if (bindGroup is null) { - ArrayPool.Shared.Return(rented); + return false; } + + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); + flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, (uint)commandCount, 0, 0); + + return true; } private BindGroup* CreateCoverageBindGroup( @@ -406,14 +418,16 @@ coverageEntry.GPUCoverageView is null || private bool TryFinalizeFlush( WebGPUFlushContext flushContext, - bool hasCpuRegion, Buffer2DRegion cpuRegion) where TPixel : unmanaged, IPixel { flushContext.EndRenderPassIfOpen(); - return flushContext.RequiresReadback - ? hasCpuRegion && this.TryReadBackToCpuRegion(flushContext, cpuRegion) - : TrySubmit(flushContext); + if (flushContext.RequiresReadback) + { + return this.TryReadBackToCpuRegion(flushContext, cpuRegion); + } + + return TrySubmit(flushContext); } private static bool TrySubmit(WebGPUFlushContext flushContext) @@ -584,80 +598,6 @@ void Callback(BufferMapAsyncStatus status, void* userData) return true; } - private bool QueueWriteTextureFromRegion( - WebGPU api, - Queue* queue, - Texture* destinationTexture, - Buffer2DRegion sourceRegion) - where TPixel : unmanaged - { - int pixelSizeInBytes = Unsafe.SizeOf(); - ImageCopyTexture destination = new() - { - Texture = destinationTexture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All - }; - - Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); - - if (IsSingleMemory(sourceRegion.Buffer) && - sourceRegion.Rectangle.X == 0 && - sourceRegion.Width == sourceRegion.Buffer.Width) - { - int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); - int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)sourceStrideBytes, - RowsPerImage = (uint)sourceRegion.Height - }; - - Span firstRow = sourceRegion.DangerousGetRowSpan(0); - fixed (TPixel* uploadPtr = firstRow) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); - } - - return true; - } - - int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - int packedByteCount = checked(packedRowBytes * sourceRegion.Height); - byte[] rented = ArrayPool.Shared.Rent(packedByteCount); - try - { - Span packedData = rented.AsSpan(0, packedByteCount); - for (int y = 0; y < sourceRegion.Height; y++) - { - ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); - MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); - } - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)packedRowBytes, - RowsPerImage = (uint)sourceRegion.Height - }; - - fixed (byte* uploadPtr = packedData) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); - } - - return true; - } - finally - { - ArrayPool.Shared.Return(rented); - } - } - /// /// Releases all cached shared WebGPU resources and fallback staging resources. /// @@ -700,7 +640,6 @@ private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memo return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEventSlim signal) { Wgpu? extension = flushContext.RuntimeLease.WgpuExtension; @@ -709,34 +648,16 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.Wait(CallbackTimeoutMilliseconds); } - Stopwatch stopwatch = Stopwatch.StartNew(); - while (!signal.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) + long start = Stopwatch.GetTimestamp(); + while (!signal.IsSet && Stopwatch.GetElapsedTime(start).TotalMilliseconds < CallbackTimeoutMilliseconds) { _ = extension.DevicePoll(flushContext.Device, true, (WrappedSubmissionIndex*)null); + if (!signal.IsSet) + { + _ = Thread.Yield(); + } } return signal.IsSet; } - - [StructLayout(LayoutKind.Sequential)] - internal struct CompositeInstanceData - { - public uint SourceOffsetX; - public uint SourceOffsetY; - public uint DestinationX; - public uint DestinationY; - public uint DestinationWidth; - public uint DestinationHeight; - public uint TargetWidth; - public uint TargetHeight; - public uint BrushKind; - public uint Padding0; - public uint Padding1; - public uint Padding2; - public Vector4 SolidBrushColor; - public float BlendPercentage; - public float Padding3; - public float Padding4; - public float Padding5; - } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 231a5a822..c70e81f03 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -29,6 +29,7 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable private bool ownsTargetTexture; private bool ownsTargetView; private bool ownsReadbackBuffer; + private WebGPUCompositeInstanceData[]? compositeInstanceData; private readonly List transientBindGroups = []; private WebGPUFlushContext( @@ -82,6 +83,23 @@ private WebGPUFlushContext( public RenderPassEncoder* PassEncoder { get; private set; } + public Span GetCompositeInstanceSpan(int count) + { + if (count <= 0) + { + return Span.Empty; + } + + WebGPUCompositeInstanceData[]? cached = this.compositeInstanceData; + if (cached is null || cached.Length < count) + { + cached = new WebGPUCompositeInstanceData[count]; + this.compositeInstanceData = cached; + } + + return cached.AsSpan(0, count); + } + public static WebGPUFlushContext Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, @@ -410,6 +428,7 @@ public void Dispose() this.ownsReadbackBuffer = false; this.ownsTargetView = false; this.ownsTargetTexture = false; + this.compositeInstanceData = null; this.RuntimeLease.Dispose(); this.disposed = true; @@ -424,15 +443,13 @@ private static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* devi } DeviceSharedState created = new(api, device); - if (DeviceStateCache.TryAdd(cacheKey, created)) + DeviceSharedState winner = DeviceStateCache.GetOrAdd(cacheKey, created); + if (!ReferenceEquals(winner, created)) { - return created; + created.Dispose(); } - created.Dispose(); - return DeviceStateCache.TryGetValue(cacheKey, out DeviceSharedState? winner) - ? winner - : GetOrCreateDeviceState(api, device); + return winner; } private static bool TryGetOrCreateSharedHandles( @@ -671,7 +688,7 @@ private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int p try { - QueueWriteTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion); + UploadTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion); } catch { @@ -747,7 +764,7 @@ private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int p return capability; } - private static void QueueWriteTextureFromRegion( + internal static void UploadTextureFromRegion( WebGPU api, Queue* queue, Texture* destinationTexture, @@ -828,6 +845,7 @@ internal sealed class DeviceSharedState : IDisposable private readonly ConcurrentDictionary compositePipelines = new(); private WebGPURasterizer? coverageRasterizer; private PipelineLayout* compositePipelineLayout; + private ShaderModule* compositeShaderModule; private bool disposed; internal DeviceSharedState(WebGPU api, Device* device) @@ -988,6 +1006,12 @@ public void Dispose() this.compositePipelineLayout = null; } + if (this.compositeShaderModule is not null) + { + this.Api.ShaderModuleRelease(this.compositeShaderModule); + this.compositeShaderModule = null; + } + if (this.CompositeBindGroupLayout is not null) { this.Api.BindGroupLayoutRelease(this.CompositeBindGroupLayout); @@ -1051,57 +1075,47 @@ private bool TryCreateCompositeInfrastructure(out string? error) return false; } + ReadOnlySpan shaderCode = CompositeCoverageShader.Code; + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + this.compositeShaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); + } + + if (this.compositeShaderModule is null) + { + error = "Failed to create composite shader module."; + return false; + } + error = null; return true; } private RenderPipeline* CreateCompositePipelineForFormat(TextureFormat textureFormat) { - if (this.compositePipelineLayout is null) + if (this.compositePipelineLayout is null || this.compositeShaderModule is null) { return null; } - ShaderModule* shaderModule = null; - try - { - ReadOnlySpan shaderCode = CompositeCoverageShader.Code; - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); - } - - if (shaderModule is null) - { - return null; - } - - ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; - ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; - fixed (byte* vertexEntryPointPtr = vertexEntryPoint) - { - fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) - { - return this.CreateCompositePipeline(shaderModule, vertexEntryPointPtr, fragmentEntryPointPtr, textureFormat); - } - } - } - finally + ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; + ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) { - if (shaderModule is not null) + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) { - this.Api.ShaderModuleRelease(shaderModule); + return this.CreateCompositePipeline(this.compositeShaderModule, vertexEntryPointPtr, fragmentEntryPointPtr, textureFormat); } } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index e2e05ad29..74d54c1f6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -67,6 +67,11 @@ private DrawingCanvas( Guard.NotNull(targetFrame, nameof(targetFrame)); Guard.NotNull(batcher, nameof(batcher)); + if (!targetFrame.TryGetCpuRegion(out _) && !targetFrame.TryGetNativeSurface(out _)) + { + throw new NotSupportedException("Canvas frame must expose either a CPU region or a native surface."); + } + this.configuration = configuration; this.backend = backend; this.targetFrame = targetFrame; From 00b45239fbab66f0730699a307446961a33acd5e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 19:02:29 +1000 Subject: [PATCH 012/136] WebGPU: support instance buffer offsets & sessions --- .../WebGPUDrawingBackend.cs | 65 +++++++++++++------ .../WebGPUFlushContext.cs | 36 ++++++---- .../Backends/DefaultDrawingBackend.cs | 1 + 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 2dae5c21a..913d78c63 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -153,22 +153,22 @@ public void FlushCompositions( return; } - bool useCpuReadbackFlushSession = hasCpuRegion && !hasNativeSurface && compositionBatch.FlushId != 0; + bool useFlushSession = compositionBatch.FlushId != 0; bool gpuSuccess = false; bool gpuReady = false; string? failure = null; - bool hadExistingCpuSession = false; + bool hadExistingSession = false; WebGPUFlushContext? flushContext = null; try { - flushContext = useCpuReadbackFlushSession - ? WebGPUFlushContext.GetOrCreateCpuReadbackFlushContext( + flushContext = useFlushSession + ? WebGPUFlushContext.GetOrCreateFlushSessionContext( compositionBatch.FlushId, target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes, - out hadExistingCpuSession) + out hadExistingSession) : WebGPUFlushContext.Create(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); CompositionCoverageDefinition definition = compositionBatch.Definition; @@ -184,11 +184,9 @@ public void FlushCompositions( gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); if (gpuSuccess) { - if (useCpuReadbackFlushSession) + if (useFlushSession && !compositionBatch.IsFinalBatchInFlush) { - gpuSuccess = compositionBatch.IsFinalBatchInFlush - ? this.TryFinalizeFlush(flushContext, cpuRegion) - : TrySubmitBatch(flushContext); + // Keep the render pass open for the next batch. } else { @@ -204,7 +202,7 @@ public void FlushCompositions( } finally { - if (!useCpuReadbackFlushSession) + if (!useFlushSession) { flushContext?.Dispose(); } @@ -215,7 +213,7 @@ public void FlushCompositions( this.TestingLastGPUInitializationFailure = gpuSuccess ? null : failure; this.TestingLiveCoverageCount = 0; - if (useCpuReadbackFlushSession) + if (useFlushSession) { if (gpuSuccess) { @@ -223,16 +221,16 @@ public void FlushCompositions( this.TestingGPUCompositeCoverageCallCount += commandCount; if (compositionBatch.IsFinalBatchInFlush) { - WebGPUFlushContext.CompleteCpuReadbackFlushContext(compositionBatch.FlushId); + WebGPUFlushContext.CompleteFlushSession(compositionBatch.FlushId); } return; } - WebGPUFlushContext.CompleteCpuReadbackFlushContext(compositionBatch.FlushId); - if (hadExistingCpuSession) + WebGPUFlushContext.CompleteFlushSession(compositionBatch.FlushId); + if (hadExistingSession) { - throw new InvalidOperationException($"WebGPU CPURegion flush session failed after prior GPU batches. Reason: {failure ?? "Unknown error"}"); + throw new InvalidOperationException($"WebGPU flush session failed after prior GPU batches. Reason: {failure ?? "Unknown error"}"); } this.TestingFallbackPrepareCoverageCallCount++; @@ -323,7 +321,25 @@ private bool TryCompositeBatch( } nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); - if (!flushContext.EnsureInstanceBufferCapacity(instanceBytes, CompositeInstanceBufferSize) || + nuint instanceOffset = flushContext.InstanceBufferWriteOffset; + nuint requiredCapacity = checked(instanceOffset + instanceBytes); + + // If the buffer exists but cannot fit at the current offset, flush pending + // draws and reset so the next batch starts at offset 0. + if (flushContext.InstanceBuffer is not null && + flushContext.InstanceBufferCapacity < requiredCapacity && + instanceOffset > 0) + { + if (!TrySubmitBatch(flushContext)) + { + return false; + } + + instanceOffset = 0; + requiredCapacity = instanceBytes; + } + + if (!flushContext.EnsureInstanceBufferCapacity(requiredCapacity, Math.Max(requiredCapacity, CompositeInstanceBufferSize)) || !flushContext.EnsureCommandEncoder() || !flushContext.BeginRenderPass()) { @@ -363,10 +379,10 @@ private bool TryCompositeBatch( fixed (WebGPUCompositeInstanceData* instancesPtr = instances) { // QueueWriteBuffer copies source bytes into driver-owned staging immediately. - flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, 0, instancesPtr, instanceBytes); + flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, instanceOffset, instancesPtr, instanceBytes); } - BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceBytes); + BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceOffset, instanceBytes); if (bindGroup is null) { return false; @@ -377,12 +393,15 @@ private bool TryCompositeBatch( flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, (uint)commandCount, 0, 0); + flushContext.AdvanceInstanceBufferOffset(instanceOffset + instanceBytes); + return true; } private BindGroup* CreateCoverageBindGroup( WebGPUFlushContext flushContext, WebGPUFlushContext.CoverageEntry coverageEntry, + nuint instanceOffset, nuint instanceBytes) { if (flushContext.DeviceState.CompositeBindGroupLayout is null || @@ -402,7 +421,7 @@ coverageEntry.GPUCoverageView is null || { Binding = 1, Buffer = flushContext.InstanceBuffer, - Offset = 0, + Offset = instanceOffset, Size = instanceBytes }; @@ -467,7 +486,13 @@ private static bool TrySubmit(WebGPUFlushContext flushContext) private static bool TrySubmitBatch(WebGPUFlushContext flushContext) { flushContext.EndRenderPassIfOpen(); - return TrySubmit(flushContext); + if (!TrySubmit(flushContext)) + { + return false; + } + + flushContext.ResetInstanceBufferOffset(); + return true; } private bool TryReadBackToCpuRegion(WebGPUFlushContext flushContext, Buffer2DRegion destinationRegion) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index c70e81f03..1380a3603 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -21,7 +21,7 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable { private static readonly ConcurrentDictionary FallbackStagingCache = new(); private static readonly ConcurrentDictionary DeviceStateCache = new(); - private static readonly ConcurrentDictionary CpuReadbackFlushContexts = new(); + private static readonly ConcurrentDictionary FlushSessionContexts = new(); private static readonly object SharedHandleSync = new(); private const int CallbackTimeoutMilliseconds = 10_000; @@ -79,6 +79,8 @@ private WebGPUFlushContext( public nuint InstanceBufferCapacity { get; private set; } + public nuint InstanceBufferWriteOffset { get; internal set; } + public CommandEncoder* CommandEncoder { get; set; } public RenderPassEncoder* PassEncoder { get; private set; } @@ -204,12 +206,12 @@ public static void ClearFallbackStagingCache() public static void ClearDeviceStateCache() { - foreach (WebGPUFlushContext context in CpuReadbackFlushContexts.Values) + foreach (WebGPUFlushContext context in FlushSessionContexts.Values) { context.Dispose(); } - CpuReadbackFlushContexts.Clear(); + FlushSessionContexts.Clear(); foreach (DeviceSharedState state in DeviceStateCache.Values) { @@ -219,7 +221,7 @@ public static void ClearDeviceStateCache() DeviceStateCache.Clear(); } - public static WebGPUFlushContext GetOrCreateCpuReadbackFlushContext( + public static WebGPUFlushContext GetOrCreateFlushSessionContext( int flushId, ICanvasFrame frame, TextureFormat expectedTextureFormat, @@ -227,7 +229,7 @@ public static WebGPUFlushContext GetOrCreateCpuReadbackFlushContext( out bool fromCache) where TPixel : unmanaged, IPixel { - if (CpuReadbackFlushContexts.TryGetValue(flushId, out WebGPUFlushContext? cached)) + if (FlushSessionContexts.TryGetValue(flushId, out WebGPUFlushContext? cached)) { fromCache = true; return cached; @@ -235,24 +237,19 @@ public static WebGPUFlushContext GetOrCreateCpuReadbackFlushContext( fromCache = false; WebGPUFlushContext created = Create(frame, expectedTextureFormat, pixelSizeInBytes); - if (!created.RequiresReadback) - { - return created; - } - - if (CpuReadbackFlushContexts.TryAdd(flushId, created)) + if (FlushSessionContexts.TryAdd(flushId, created)) { return created; } created.Dispose(); fromCache = true; - return CpuReadbackFlushContexts[flushId]; + return FlushSessionContexts[flushId]; } - public static void CompleteCpuReadbackFlushContext(int flushId) + public static void CompleteFlushSession(int flushId) { - if (CpuReadbackFlushContexts.TryRemove(flushId, out WebGPUFlushContext? context)) + if (FlushSessionContexts.TryRemove(flushId, out WebGPUFlushContext? context)) { context.Dispose(); } @@ -397,6 +394,8 @@ public void Dispose() this.InstanceBufferCapacity = 0; } + this.InstanceBufferWriteOffset = 0; + if (this.ownsReadbackBuffer && this.ReadbackBuffer is not null) { this.Api.BufferRelease(this.ReadbackBuffer); @@ -836,9 +835,18 @@ internal static void UploadTextureFromRegion( } } + public void ResetInstanceBufferOffset() + => this.InstanceBufferWriteOffset = 0; + + public void AdvanceInstanceBufferOffset(nuint newOffset) + => this.InstanceBufferWriteOffset = AlignToStorageBufferOffset(newOffset); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nuint AlignToStorageBufferOffset(nuint value) => (value + 255) & ~(nuint)255; + internal sealed class DeviceSharedState : IDisposable { private readonly Dictionary coverageCache = []; diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 2ffc3ef26..d09727b01 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -148,6 +148,7 @@ private Buffer2D CreateCoverageMap( public void Dispose() { + // WTF is this here for? GC.KeepAlive(this.PrimaryRasterizer); } } From d4ff9e10a0096d29d5f3e9bdfb3c573f1a3570f1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 20:23:04 +1000 Subject: [PATCH 013/136] Remove Configuration from FillPath; improve batching --- .../WebGPUDrawingBackend.cs | 6 +----- .../Backends/DefaultDrawingBackend.cs | 11 ++-------- .../Processing/Backends/IDrawingBackend.cs | 2 -- .../DrawingCanvasBatcher{TPixel}.cs | 1 - .../Processing/DrawingCanvas{TPixel}.cs | 21 ++++++++++++++++--- .../Drawing/DrawTextRepeatedGlyphs.cs | 12 ++++++++--- .../Backends/SkiaCoverageDrawingBackend.cs | 1 - .../Processing/DrawingCanvasBatcherTests.cs | 1 - .../RasterizerDefaultsExtensionsTests.cs | 1 - 9 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 913d78c63..3976993ba 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -105,7 +105,6 @@ internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) /// public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, @@ -143,8 +142,6 @@ public void FlushCompositions( this.TestingCompositeCoverageCallCount += commandCount; bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); - bool hasNativeSurface = target.TryGetNativeSurface(out _); - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) { this.TestingFallbackPrepareCoverageCallCount++; @@ -177,8 +174,7 @@ public void FlushCompositions( in definition, out RenderPipeline* pipeline, out WebGPUFlushContext.CoverageEntry? coverageEntry, - out failure) && - coverageEntry is not null) + out failure)) { gpuReady = true; gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index d09727b01..c5e7038e4 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -44,7 +44,6 @@ public static DefaultDrawingBackend Create(IRasterizer rasterizer) /// public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, @@ -117,9 +116,9 @@ public void FlushCompositions( } finally { - for (int i = 0; i < applicators.Length; i++) + foreach (BrushApplicator? applicator in applicators) { - applicators[i]?.Dispose(); + applicator?.Dispose(); } } } @@ -145,10 +144,4 @@ private Buffer2D CreateCoverageMap( return coverage; } - - public void Dispose() - { - // WTF is this here for? - GC.KeepAlive(this.PrimaryRasterizer); - } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index d23d3ca65..a65c07254 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -18,7 +18,6 @@ internal interface IDrawingBackend /// Fills a path into a destination target region. /// /// The pixel format. - /// Active processing configuration. /// Destination frame. /// Path in target-local coordinates. /// Brush used to shade covered pixels. @@ -26,7 +25,6 @@ internal interface IDrawingBackend /// Rasterizer options in target-local coordinates. /// Batcher used to queue normalized composition commands. public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 2567b4f66..17ee96fd5 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing; diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 74d54c1f6..f3cefc467 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -182,7 +182,6 @@ internal void FillPath( samplingOrigin); this.backend.FillPath( - this.configuration, this.targetFrame, path, brush, @@ -259,9 +258,25 @@ private void DrawTextOperations(IEnumerable operations, Drawin Guard.NotNull(operations, nameof(operations)); Guard.NotNull(drawingOptions, nameof(drawingOptions)); - foreach (DrawingOperation operation in operations.OrderBy(x => x.RenderPass)) + // Build composition commands and sort by render pass then definition key so that + // same-coverage glyph variants are contiguous. Text glyphs within the same render + // pass occupy non-overlapping positions, making this reordering visually safe while + // maximizing batch sizes in the downstream batcher. + List<(byte RenderPass, CompositionCommand Command)> entries = []; + foreach (DrawingOperation operation in operations) { - this.batcher.AddComposition(this.CreateCompositionCommand(operation, drawingOptions)); + entries.Add((operation.RenderPass, this.CreateCompositionCommand(operation, drawingOptions))); + } + + entries.Sort(static (a, b) => + { + int cmp = a.RenderPass.CompareTo(b.RenderPass); + return cmp != 0 ? cmp : a.Command.DefinitionKey.CompareTo(b.Command.DefinitionKey); + }); + + foreach ((_, CompositionCommand command) in entries) + { + this.batcher.AddComposition(command); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 4b6ee94b9..a4ceb951a 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -52,6 +52,13 @@ public class DrawTextRepeatedGlyphs [GlobalSetup] public void Setup() { + // Tiled rasterization benefits from a warmed worker pool. Doing this once in setup + // reduces first-iteration noise without affecting per-method correctness. + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount); + ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads); + Parallel.For(0, desiredWorkerThreads, static _ => { }); + Font font = SystemFonts.CreateFont("Arial", 48); this.textOptions = new RichTextOptions(font) { @@ -178,16 +185,15 @@ public bool TryGetNativeSurface(out NativeSurface surface) private sealed class NativeSurfaceOnlyFrame : ICanvasFrame where TPixel : unmanaged, IPixel { - private readonly Rectangle bounds; private readonly NativeSurface surface; public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) { - this.bounds = bounds; + this.Bounds = bounds; this.surface = surface; } - public Rectangle Bounds => this.bounds; + public Rectangle Bounds { get; } public bool TryGetCpuRegion(out Buffer2DRegion region) { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index c59c690f1..7ebf0719d 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -27,7 +27,6 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 0ee8348ed..8cd75c8d9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -52,7 +52,6 @@ private sealed class CapturingBackend : IDrawingBackend Array.Empty()); public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 8124edfaf..dd82f4c28 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -110,7 +110,6 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { public void FillPath( - Configuration configuration, ICanvasFrame target, IPath path, Brush brush, From 4d807df778ec3bfde3b159c95785337db947f913 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 20:31:20 +1000 Subject: [PATCH 014/136] Use List and pre-size entries in DrawTextOperations --- src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index f3cefc467..d0c9d6258 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -252,7 +252,7 @@ public void DrawText( this.DrawTextOperations(textRenderer.DrawingOperations, drawingOptions); } - private void DrawTextOperations(IEnumerable operations, DrawingOptions drawingOptions) + private void DrawTextOperations(List operations, DrawingOptions drawingOptions) { this.EnsureNotDisposed(); Guard.NotNull(operations, nameof(operations)); @@ -262,9 +262,10 @@ private void DrawTextOperations(IEnumerable operations, Drawin // same-coverage glyph variants are contiguous. Text glyphs within the same render // pass occupy non-overlapping positions, making this reordering visually safe while // maximizing batch sizes in the downstream batcher. - List<(byte RenderPass, CompositionCommand Command)> entries = []; - foreach (DrawingOperation operation in operations) + List<(byte RenderPass, CompositionCommand Command)> entries = new(operations.Count); + for (int i = 0; i < operations.Count; i++) { + DrawingOperation operation = operations[i]; entries.Add((operation.RenderPass, this.CreateCompositionCommand(operation, drawingOptions))); } From 3255a1b40043cb368c1e553a4b7feb802c6b8ecc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 21:21:26 +1000 Subject: [PATCH 015/136] Cache path definition keys to avoid flattening --- .../Processing/Backends/CompositionCommand.cs | 43 +++++++++++++++++-- .../Processing/DrawingCanvas{TPixel}.cs | 13 +++--- .../Drawing/DrawTextRepeatedGlyphs.cs | 29 ++++--------- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index def345965..89e2085e5 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -81,15 +82,17 @@ private CompositionCommand( /// Graphics options used for composition. /// Rasterizer options used to generate coverage. /// Absolute destination offset where coverage is composited. + /// Optional scoped cache to avoid repeated path flattening for the same reference. /// The normalized composition command. public static CompositionCommand Create( IPath path, Brush brush, GraphicsOptions graphicsOptions, in RasterizerOptions rasterizerOptions, - Point destinationOffset = default) + Point destinationOffset = default, + Dictionary? definitionKeyCache = null) { - int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions); + int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions, definitionKeyCache); RectangleF bounds = path.Bounds; Rectangle localBrushBounds = Rectangle.FromLTRB( (int)MathF.Floor(bounds.Left), @@ -117,8 +120,42 @@ public static CompositionCommand Create( /// /// Path to rasterize. /// Rasterizer options used for coverage generation. + /// Optional scoped cache keyed by path identity to avoid repeated path flattening. /// A stable key for coverage-equivalent commands. - public static int ComputeCoverageDefinitionKey(IPath path, in RasterizerOptions rasterizerOptions) + public static int ComputeCoverageDefinitionKey( + IPath path, + in RasterizerOptions rasterizerOptions, + Dictionary? definitionKeyCache = null) + { + // Fast path: when the caller provides a cache and the same IPath object is + // reused (e.g. cached glyph sub-pixel variants), skip the expensive + // Flatten + point-hash and return the cached key. + if (definitionKeyCache is not null) + { + int pathIdentity = RuntimeHelpers.GetHashCode(path); + int rasterState = HashCode.Combine( + rasterizerOptions.Interest.Size, + (int)rasterizerOptions.IntersectionRule, + (int)rasterizerOptions.RasterizationMode, + (int)rasterizerOptions.SamplingOrigin); + int cacheProbe = HashCode.Combine(pathIdentity, rasterState); + + if (definitionKeyCache.TryGetValue(cacheProbe, out (IPath Path, int RasterState, int DefinitionKey) cached) && + ReferenceEquals(cached.Path, path) && + cached.RasterState == rasterState) + { + return cached.DefinitionKey; + } + + int definitionKey = ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions); + definitionKeyCache[cacheProbe] = (path, rasterState, definitionKey); + return definitionKey; + } + + return ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions); + } + + private static int ComputeCoverageDefinitionKeySlow(IPath path, in RasterizerOptions rasterizerOptions) { HashCode hash = default; foreach (ISimplePath simplePath in path.Flatten()) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index d0c9d6258..1b3c811ed 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -262,11 +262,12 @@ private void DrawTextOperations(List operations, DrawingOption // same-coverage glyph variants are contiguous. Text glyphs within the same render // pass occupy non-overlapping positions, making this reordering visually safe while // maximizing batch sizes in the downstream batcher. + Dictionary definitionKeyCache = []; List<(byte RenderPass, CompositionCommand Command)> entries = new(operations.Count); for (int i = 0; i < operations.Count; i++) { DrawingOperation operation = operations[i]; - entries.Add((operation.RenderPass, this.CreateCompositionCommand(operation, drawingOptions))); + entries.Add((operation.RenderPass, this.CreateCompositionCommand(operation, drawingOptions, definitionKeyCache))); } entries.Sort(static (a, b) => @@ -275,9 +276,9 @@ private void DrawTextOperations(List operations, DrawingOption return cmp != 0 ? cmp : a.Command.DefinitionKey.CompareTo(b.Command.DefinitionKey); }); - foreach ((_, CompositionCommand command) in entries) + for (int i = 0; i < entries.Count; i++) { - this.batcher.AddComposition(command); + this.batcher.AddComposition(entries[i].Command); } } @@ -323,7 +324,8 @@ private static RichTextOptions ConfigureTextOptions(RichTextOptions options) private CompositionCommand CreateCompositionCommand( DrawingOperation operation, - DrawingOptions drawingOptions) + DrawingOptions drawingOptions, + Dictionary? definitionKeyCache = null) { Brush compositeBrush = operation.Kind == DrawingOperationKind.Fill ? operation.Brush! @@ -387,6 +389,7 @@ private CompositionCommand CreateCompositionCommand( compositeBrush, graphicsOptions, rasterizerOptions, - destinationOffset); + destinationOffset, + definitionKeyCache); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index a4ceb951a..b1e181bef 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; [MemoryDiagnoser] +[WarmupCount(5)] +[IterationCount(15)] public class DrawTextRepeatedGlyphs { public const int Width = 1200; @@ -95,24 +97,6 @@ public void Setup() this.text = new string('A', this.GlyphCount); } - [IterationSetup(Target = nameof(DrawingCanvasDefaultBackend))] - public void IterationSetupDefault() - => this.ClearWithDrawingCanvas( - this.defaultConfiguration, - new CpuRegionOnlyFrame(GetFrameRegion(this.defaultImage))); - - [IterationSetup(Target = nameof(DrawingCanvasWebGPUBackendCpuRegion))] - public void IterationSetupWebGpuCpuRegion() - => this.ClearWithDrawingCanvas( - this.webGpuConfiguration, - new CpuRegionOnlyFrame(GetFrameRegion(this.webGpuCpuImage))); - - [IterationSetup(Target = nameof(DrawingCanvasWebGPUBackendNativeSurface))] - public void IterationSetupWebGpuNativeSurface() - => this.ClearWithDrawingCanvas( - this.webGpuConfiguration, - this.webGpuNativeFrame); - [GlobalCleanup] public void Cleanup() { @@ -129,7 +113,9 @@ public void Cleanup() [Benchmark(Baseline = true, Description = "DrawingCanvas Default Backend")] public void DrawingCanvasDefaultBackend() { - using DrawingCanvas canvas = new(this.defaultConfiguration, new CpuRegionOnlyFrame(GetFrameRegion(this.defaultImage))); + CpuRegionOnlyFrame frame = new(GetFrameRegion(this.defaultImage)); + this.ClearWithDrawingCanvas(this.defaultConfiguration, frame); + using DrawingCanvas canvas = new(this.defaultConfiguration, frame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); } @@ -137,7 +123,9 @@ public void DrawingCanvasDefaultBackend() [Benchmark(Description = "DrawingCanvas WebGPU Backend (CPURegion)")] public void DrawingCanvasWebGPUBackendCpuRegion() { - using DrawingCanvas canvas = new(this.webGpuConfiguration, new CpuRegionOnlyFrame(GetFrameRegion(this.webGpuCpuImage))); + CpuRegionOnlyFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); + this.ClearWithDrawingCanvas(this.webGpuConfiguration, frame); + using DrawingCanvas canvas = new(this.webGpuConfiguration, frame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); } @@ -145,6 +133,7 @@ public void DrawingCanvasWebGPUBackendCpuRegion() [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] public void DrawingCanvasWebGPUBackendNativeSurface() { + this.ClearWithDrawingCanvas(this.webGpuConfiguration, this.webGpuNativeFrame); using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); From 4915396e56a76316dcac51445230b09b5eeb7eb8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Feb 2026 22:42:56 +1000 Subject: [PATCH 016/136] Add glyph cache, layer & path handling --- .../Processors/Text/RichTextGlyphRenderer.cs | 324 +++++++++++++++++- .../Shapes/Text/BaseGlyphBuilder.cs | 163 +++++++-- .../Shapes/Text/GlyphBuilder.cs | 4 +- .../Shapes/Text/PathGlyphBuilder.cs | 29 +- 4 files changed, 480 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index 45b403fcb..d7e0a187e 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -15,38 +15,98 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; /// internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable { + // --- Render-pass ordering constants --- + // Within DrawTextOperations, operations are sorted first by RenderPass so that + // fills paint beneath outlines, and outlines beneath decorations. private const byte RenderOrderFill = 0; private const byte RenderOrderOutline = 1; private const byte RenderOrderDecoration = 2; private readonly DrawingOptions drawingOptions; + + /// The default pen supplied by the caller (e.g. from DrawText(..., pen)). private readonly Pen? defaultPen; + + /// The default brush supplied by the caller (e.g. from DrawText(..., brush)). private readonly Brush? defaultBrush; + + /// + /// When the text is laid out along a path, this holds the path internals + /// for point-along-path queries. for normal (linear) text. + /// private readonly IPathInternals? path; private bool isDisposed; + // --- Per-glyph mutable state reset in BeginGlyph --- + + /// The (or ) governing the current glyph. private TextRun? currentTextRun; + + /// Brush resolved from the current , or . private Brush? currentBrush; + + /// Pen resolved from the current , or . private Pen? currentPen; + + /// The fill rule for the current color layer (COLR). private FillRule currentFillRule; + + /// Alpha composition mode active for the current glyph/layer. private PixelAlphaCompositionMode currentCompositionMode; + + /// Color blending mode active for the current glyph/layer. private PixelColorBlendingMode currentBlendingMode; + + /// Whether the current glyph uses vertical layout (affects decoration orientation). private bool currentDecorationIsVertical; + + /// Set to when is called, cleared in . private bool hasLayer; - // Just enough accuracy to allow for 1/8 px differences which later are accumulated while rendering, - // but do not grow into full px offsets. - // The value 8 is benchmarked to: - // - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant) - // - Cache hit ratio above 60% + // --- Glyph outline cache --- + // Glyphs that share the same CacheKey (same glyph id, sub-pixel position quantized + // to 1/AccuracyMultiple, pen reference, etc.) reuse the translated IPath from the + // first occurrence. This avoids re-building the full outline for repeated characters. + // + // AccuracyMultiple = 8 means sub-pixel positions are quantized to 1/8 px steps. + // Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio. private const float AccuracyMultiple = 8; + + /// Maps cache keys to their list of entries (one per layer). private readonly Dictionary> glyphCache = []; + + /// Read cursor into the cached layer list for layered cache hits. private int cacheReadIndex; + /// + /// when the current glyph is a cache miss and its outline + /// must be fully rasterized; on a cache hit (reuse path). + /// private bool rasterizationRequired; + + /// + /// to disable the glyph cache entirely (e.g. path-based text + /// where every glyph has a unique transform). + /// private readonly bool noCache; + + /// The cache key computed for the current glyph in . private CacheKey currentCacheKey; + /// + /// The transformed (post-) bounding-box location + /// of the current glyph. Stored so can compute + /// for future cache-hit render location estimation. + /// + private PointF currentTransformedBoundsLocation; + + /// + /// Initializes a new instance of the class. + /// + /// Rich text options that may include a layout path and text runs. + /// Drawing options (transform, graphics options) for the text block. + /// Default pen for outlined text, or for fill-only. + /// Default brush for filled text, or for outline-only. public RichTextGlyphRenderer( RichTextOptions textOptions, DrawingOptions drawingOptions, @@ -64,7 +124,8 @@ public RichTextGlyphRenderer( IPath? path = textOptions.Path; if (path is not null) { - // Turn off caching. The chances of a hit are near-zero. + // Path-based text: each glyph gets a unique per-position transform, + // so cache hits are near-impossible — disable caching entirely. this.rasterizationRequired = true; this.noCache = true; if (path is IPathInternals internals) @@ -78,15 +139,24 @@ public RichTextGlyphRenderer( } } + /// + /// Gets the list of instances accumulated during text rendering. + /// After RenderText completes, this list is consumed by + /// to build composition commands. + /// public List DrawingOperations { get; } /// protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear(); /// - protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { - // Reset state. + // Resolves the active brush/pen from the text run, computes the cache key, + // and takes one of three paths: + // 1. Non-layered cache hit without decorations → emit cached ops, return false (fast path). + // 2. Layered or decorated cache hit → reuse cached path, return true for EndGlyph/SetDecoration. + // 3. Cache miss → rasterize from scratch. this.cacheReadIndex = 0; this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; this.currentTextRun = parameters.TextRun; @@ -103,13 +173,15 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara if (!this.noCache) { - // Create a cache entry for the glyph. - // We need to apply the default transform to the bounds to get the correct size - // for comparison with future glyphs. We can use this cached glyph anywhere in the text block. + // Transform the font-metric bounds by the drawing transform so that the + // sub-pixel position and size reflect the final screen coordinates. + // Quantize to 1/AccuracyMultiple px steps for cache key comparison. RectangleF currentBounds = RectangleF.Transform( new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)), this.drawingOptions.Transform); + this.currentTransformedBoundsLocation = currentBounds.Location; + PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location); PointF subPixelLocation = new( MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple, @@ -124,11 +196,22 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara new RectangleF(subPixelLocation, subPixelSize), this.currentPen ?? this.defaultPen); - if (this.glyphCache.ContainsKey(this.currentCacheKey)) + if (this.glyphCache.TryGetValue(this.currentCacheKey, out List? cachedEntries)) { - // We have already drawn the glyph vectors. + if (cachedEntries.Count > 0 && !cachedEntries[0].IsLayered + && this.EnabledDecorations() == TextDecorations.None) + { + // Non-layered cache hit without decorations: emit operations directly + // and tell the font engine to skip the outline entirely + // (no MoveTo/LineTo/SetDecoration/EndGlyph). + this.EmitCachedGlyphOperations(cachedEntries[0], currentBounds.Location); + return false; + } + + // Layered or decorated cache hit: let the normal flow handle + // per-layer state and decoration callbacks. this.rasterizationRequired = false; - return; + return true; } } @@ -136,10 +219,14 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara // The default transform will automatically be applied. this.TransformGlyph(in bounds); this.rasterizationRequired = true; + return true; } + /// protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { + // Capture the color-layer paint, fill rule, and composite mode. + // Setting hasLayer tells EndGlyph to skip its default single-layer path emission. this.hasLayer = true; this.currentFillRule = fillRule; if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush)) @@ -150,8 +237,12 @@ protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? cl } } + /// protected override void EndLayer() { + // Finalizes a color layer. On a cache miss, translates the built path to local + // coordinates and stores it for future hits. On a cache hit, reads the stored + // path and adjusts the render location using sub-pixel delta compensation. GlyphRenderData renderData = default; IPath? fillPath = null; @@ -186,6 +277,7 @@ protected override void EndLayer() // Capture the delta between the location and the truncated render location. // We can use this to offset the render location on the next instance of this glyph. renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + renderData.IsLayered = true; if (!this.noCache) { @@ -251,8 +343,12 @@ protected override void EndLayer() this.currentBlendingMode = this.drawingOptions.GraphicsOptions.ColorBlendingMode; } + /// public override TextDecorations EnabledDecorations() { + // Returns the union of decorations from TextRun.TextDecorations and any + // decoration pens set on the current RichTextRun. The font engine uses + // this result to decide which SetDecoration calls to emit. TextRun? run = this.currentTextRun; TextDecorations decorations = run?.TextDecorations ?? TextDecorations.None; @@ -277,8 +373,14 @@ public override TextDecorations EnabledDecorations() return decorations; } + /// public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { + // Emits a DrawingOperation for a text decoration. Resolves the decoration pen + // from the current RichTextRun, re-scales the base-class path when the pen's + // stroke width differs from the font-metric thickness, and anchors the scaling + // per decoration type (overline→bottom edge, underline→top edge, strikeout→center). + // Decorations are not cached. if (thickness == 0) { return; @@ -371,8 +473,13 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star }); } + /// protected override void EndGlyph() { + // If hasLayer is set, layers were already handled by EndLayer — skip. + // Otherwise, on a cache miss the built path is translated to local coordinates, + // stored for future hits, and emitted as fill and/or outline DrawingOperations. + // On a cache hit the stored path is reused with sub-pixel delta compensation. if (this.hasLayer) { // The layer has already been rendered. @@ -427,6 +534,10 @@ protected override void EndGlyph() // We can use this to offset the render location on the next instance of this glyph. renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + // Store the offset between outline bounds and font metric bounds so that + // cache hits in BeginGlyph can accurately estimate the path location. + renderData.BoundsOffset = (Vector2)(path.Bounds.Location - this.currentTransformedBoundsLocation); + if (!this.noCache) { this.UpdateCache(renderData); @@ -508,6 +619,109 @@ protected override void EndGlyph() } } + /// + /// Emits fill and/or outline s from a cached + /// entry. Called from on a + /// non-layered, decoration-free cache hit when the font engine is told to skip + /// the outline entirely (returns ). + /// + /// The cached render data containing the translated path and location delta. + /// The transformed bounding-box origin for the current glyph instance. + private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF currentBoundsLocation) + { + // Estimate the outline bounds location using the stored offset between + // the outline bounds and the font metric bounds from the original glyph. + PointF estimatedPathLocation = new( + currentBoundsLocation.X + renderData.BoundsOffset.X, + currentBoundsLocation.Y + renderData.BoundsOffset.Y); + Point renderLocation = ComputeCacheHitRenderLocation(estimatedPathLocation, renderData.LocationDelta); + + // Fix up the text runs colors. + Brush? brush = this.currentBrush; + Pen? pen = this.currentPen; + if (brush == null && pen == null) + { + brush = this.defaultBrush; + pen = this.defaultPen; + } + + IPath? glyphPath = renderData.FillPath; + if (glyphPath is null) + { + return; + } + + if (brush != null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = brush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + if (pen != null) + { + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = pen, + RenderPass = RenderOrderOutline, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + } + + /// + /// Computes the pixel-snapped render location for a cache-hit glyph by compensating + /// for the sub-pixel delta difference between the original cached glyph and the + /// current instance. This keeps glyphs visually aligned even when their sub-pixel + /// positions differ slightly. + /// + /// The estimated outline bounds origin for the current glyph. + /// The sub-pixel delta recorded when the path was first cached. + /// A pixel-snapped render location. + private static Point ComputeCacheHitRenderLocation(PointF pathLocation, Vector2 previousDelta) + { + Vector2 currentLocation = (Vector2)pathLocation; + Vector2 currentDelta = currentLocation - (Vector2)ClampToPixel(pathLocation); + + if (previousDelta.Y > currentDelta.Y) + { + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + return ClampToPixel(currentLocation); + } + + /// + /// Stores a entry in the glyph cache under the + /// current key. Creates the cache list on first insertion for a given key. + /// private void UpdateCache(GlyphRenderData renderData) { if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List? _)) @@ -518,15 +732,28 @@ private void UpdateCache(GlyphRenderData renderData) this.glyphCache[this.currentCacheKey].Add(renderData); } + /// public void Dispose() => this.Dispose(true); + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Point ClampToPixel(PointF point) => Point.Truncate(point); + /// + /// Applies the path-based transform to the + /// for the current glyph, positioning it along the text path (if any) or + /// leaving the identity transform for linear text. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void TransformGlyph(in FontRectangle bounds) => this.Builder.SetTransform(this.ComputeTransform(in bounds)); + /// + /// Computes the combined translation + rotation matrix that places a glyph + /// along the text path. For linear text (no path), returns . + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private Matrix3x2 ComputeTransform(in FontRectangle bounds) { @@ -545,6 +772,10 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds) return Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); } + /// + /// Releases managed resources (glyph cache and drawing operations list). + /// + /// to release managed resources. private void Dispose(bool disposing) { if (!this.isDisposed) @@ -559,47 +790,112 @@ private void Dispose(bool disposing) } } + /// + /// Per-layer cached data for a rasterized glyph. Stores the locally-translated + /// path and the sub-pixel deltas needed to reposition the path at a different + /// screen location on a cache hit. + /// private struct GlyphRenderData { + /// + /// The fractional-pixel offset between the path's bounding-box origin + /// and the truncated (pixel-snapped) render location. Used to compensate + /// for sub-pixel position differences between cache hits. + /// public Vector2 LocationDelta; + /// + /// The offset between the outline path's bounding-box origin and the + /// font-metric bounds origin. Stored on first rasterization so that + /// can estimate the path location + /// from only the font-metric bounds (which are available without outline data). + /// + public Vector2 BoundsOffset; + + /// + /// The glyph outline path translated to local coordinates (origin at 0,0). + /// Shared across all cache hits for the same . + /// public IPath? FillPath; + + /// + /// if this entry belongs to a multi-layer (COLR) glyph. + /// Non-layered cache hits with no decorations can skip the outline entirely + /// (return from ); layered hits + /// still need the per-layer BeginLayer/EndLayer callbacks. + /// + public bool IsLayered; } + /// + /// Identifies a unique glyph variant for caching purposes. Two glyphs with the same + /// share identical outline geometry and can reuse the same + /// . The key includes the glyph id, font metrics, + /// sub-pixel position (quantized to ), and the pen reference + /// (since stroke width affects the outline path). + /// private readonly struct CacheKey : IEquatable { + /// Gets the font family name. public string Font { get; init; } + /// Gets the glyph color variant (normal, COLR, etc.). public GlyphColor GlyphColor { get; init; } + /// Gets the glyph type (simple, composite, etc.). public GlyphType GlyphType { get; init; } + /// Gets the font style (regular, bold, italic, etc.). public FontStyle FontStyle { get; init; } + /// Gets the glyph index within the font. public ushort GlyphId { get; init; } + /// Gets the composite glyph parent index (0 for non-composite). public ushort CompositeGlyphId { get; init; } + /// Gets the Unicode code point this glyph represents. public CodePoint CodePoint { get; init; } + /// Gets the em-size at which the glyph is rendered. public float PointSize { get; init; } + /// Gets the DPI used for rendering. public float Dpi { get; init; } + /// Gets the layout mode (horizontal, vertical, vertical-rotated). public GlyphLayoutMode LayoutMode { get; init; } + /// Gets any text attributes (e.g. superscript/subscript) that affect rendering. public TextAttributes TextAttributes { get; init; } + /// Gets text decorations that may influence outline geometry. public TextDecorations TextDecorations { get; init; } + /// Gets the quantized sub-pixel bounds used for position-sensitive cache lookup. public RectangleF Bounds { get; init; } + /// + /// Gets the pen reference used for outlined text. Compared by reference equality + /// so that different pen instances (even with the same stroke width) produce + /// separate cache entries — this is correct because pen identity affects stroke + /// pattern and dash style. + /// public Pen? PenReference { get; init; } public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); + /// + /// Creates a from glyph renderer parameters and quantized bounds. + /// The grapheme index is intentionally excluded because it varies per glyph instance + /// while the outline geometry remains the same for matching glyph+position. + /// + /// The glyph renderer parameters from the font engine. + /// Quantized sub-pixel bounds for position-sensitive lookup. + /// The pen reference for outlined text, or . + /// A new cache key. public static CacheKey FromParameters( in GlyphRendererParameters parameters, RectangleF bounds, diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index 2966e3f7a..846d7e79f 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -14,20 +14,39 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// internal class BaseGlyphBuilder : IGlyphRenderer { + /// + /// The last point emitted by MoveTo / LineTo / curve commands. + /// Used as the implicit start of the next segment. + /// private Vector2 currentPoint; + + /// + /// Snapshot of the for the glyph currently + /// being processed. Set at the start of each BeginGlyph call and read by + /// SetDecoration to determine layout orientation. + /// private GlyphRendererParameters parameters; // Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph. + // When true, EndGlyph skips its default single-layer path capture because layers + // already contributed their paths individually. private bool usedLayers; // Tracks whether we are currently inside a layer block. + // Guards against unbalanced EndLayer calls. private bool inLayer; - // Per-GRAPHEME layered capture (aggregate multiple glyphs of the same grapheme, e.g. COLR v0 layers): + // --- Per-GRAPHEME layered capture --- + // A grapheme cluster (e.g. a base glyph + COLR v0 color layers) may span + // multiple BeginGlyph/EndGlyph calls. These fields aggregate all layers + // belonging to the same grapheme into a single GlyphPathCollection. private GlyphPathCollection.Builder? graphemeBuilder; private int graphemePathCount; private int currentGraphemeIndex = -1; private readonly List currentGlyphs = []; + + // Previous decoration details per decoration type, used to stitch adjacent + // decorations together and eliminate sub-pixel gaps between glyphs. private TextDecorationDetails? previousUnderlineTextDecoration; private TextDecorationDetails? previousOverlineTextDecoration; private TextDecorationDetails? previousStrikeoutTextDecoration; @@ -38,8 +57,17 @@ internal class BaseGlyphBuilder : IGlyphRenderer private FillRule currentLayerFillRule; private ClipQuad? currentClipBounds; + /// + /// Initializes a new instance of the class + /// with an identity transform. + /// public BaseGlyphBuilder() => this.Builder = new PathBuilder(); + /// + /// Initializes a new instance of the class + /// with the specified transform applied to all incoming glyph geometry. + /// + /// A matrix transform applied to every point received from the font engine. public BaseGlyphBuilder(Matrix3x2 transform) => this.Builder = new PathBuilder(transform); /// @@ -53,13 +81,25 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// public IReadOnlyList Glyphs => this.currentGlyphs; + /// + /// Gets the used to accumulate outline segments + /// (MoveTo, LineTo, curves) for the current glyph or layer. + /// The builder is cleared between glyphs / layers. + /// protected PathBuilder Builder { get; } /// - /// Gets the paths captured for the current glyph/grapheme. + /// Gets the running list of all instances produced so far + /// (glyph outlines, layer outlines, and decoration rectangles). Subclasses + /// read from the end of this list (e.g. CurrentPaths[^1]) to obtain + /// the most recently built path. /// protected List CurrentPaths { get; } = []; + /// + /// Called by the font engine after all glyphs in the text block have been rendered. + /// Flushes any in-progress grapheme aggregate and resets per-text-block state. + /// void IGlyphRenderer.EndText() { // Finalize the last grapheme, if any: @@ -80,6 +120,15 @@ void IGlyphRenderer.EndText() void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds); + /// + /// Called by the font engine before emitting outline data for a single glyph. + /// Manages grapheme-cluster transitions and resets per-glyph state. + /// + /// + /// to have the font engine emit the full outline + /// (MoveTo/LineTo/curves/EndGlyph); to skip it entirely, + /// which is used by caching subclasses when the glyph path is already available. + /// bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { // If grapheme changed, flush previous aggregate and start a new one: @@ -110,8 +159,7 @@ bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParamete this.currentLayerPaint = null; this.currentLayerFillRule = FillRule.NonZero; this.currentClipBounds = null; - this.BeginGlyph(in bounds, in parameters); - return true; + return this.BeginGlyph(in bounds, in parameters); } /// @@ -124,10 +172,15 @@ void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdContr this.currentPoint = point; } - /// + /// + /// Called by the font engine after the outline for a single glyph has been fully emitted. + /// Builds the accumulated path and registers it as a grapheme layer unless explicit + /// BeginLayer/EndLayer pairs already handled layer registration. + /// void IGlyphRenderer.EndGlyph() { - // If the glyph did not open any explicit layer, treat its geometry as a single layer in the current grapheme: + // If the glyph did not open any explicit layer, treat its geometry as a single + // implicit layer so that non-color glyphs still produce a GlyphPathCollection entry. if (!this.usedLayers) { IPath path = this.Builder.Build(); @@ -187,7 +240,10 @@ void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) this.currentPoint = point; } - /// + /// + /// Called by the font engine to begin a color layer within a COLR v0/v1 glyph. + /// Each layer receives its own paint, fill rule, and optional clip bounds. + /// void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { this.usedLayers = true; @@ -201,7 +257,11 @@ void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBo this.BeginLayer(paint, fillRule, clipBounds); } - /// + /// + /// Called by the font engine to close a color layer opened by BeginLayer. + /// Builds the layer path, applies any clip quad, and registers the result + /// as a painted layer in the current grapheme aggregate. + /// void IGlyphRenderer.EndLayer() { if (!this.inLayer) @@ -211,6 +271,8 @@ void IGlyphRenderer.EndLayer() IPath path = this.Builder.Build(); + // If the layer defines a clip quad (e.g. from COLR v1), intersect the + // built path with the quad polygon to constrain rendering. if (this.currentClipBounds is not null) { ClipQuad clip = this.currentClipBounds.Value; @@ -251,7 +313,13 @@ void IGlyphRenderer.EndLayer() this.EndLayer(); } - /// + /// + /// Called by the font engine to emit a text decoration (underline, strikeout, or overline) + /// for the current glyph. Builds a filled rectangle path from the start/end positions and + /// thickness, then registers it as a layer. + /// Adjacent decorations are stitched together using the previous decoration details to + /// eliminate sub-pixel gaps caused by font metric rounding. + /// void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { if (thickness == 0) @@ -392,52 +460,100 @@ protected virtual void BeginText(in FontRectangle bounds) { } - /// - protected virtual void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - { - } + /// + /// Called after base-class bookkeeping in IGlyphRenderer.BeginGlyph. + /// Subclasses override this to apply transforms, consult caches, or opt out of + /// outline emission by returning . + /// + /// The font-metric bounding rectangle of the glyph. + /// Identifies the glyph (id, font, layout mode, text run, etc.). + /// + /// to receive outline data and an EndGlyph call; + /// to skip outline emission for this glyph entirely. + /// + protected virtual bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + => true; - /// + /// + /// Called after the base class has built and registered the glyph path. + /// Subclasses override this to emit drawing operations from the captured path. + /// protected virtual void EndGlyph() { } - /// + /// + /// Called after the base class has flushed all grapheme aggregates. + /// Subclasses override this for any per-text-block finalization. + /// protected virtual void EndText() { } - /// + /// + /// Called when a COLR color layer begins. Subclasses override this to + /// capture the layer's paint and composite mode. + /// + /// The paint for this color layer, or for the default foreground. + /// The fill rule to use when rasterizing this layer. + /// Optional clip quad constraining the layer region. protected virtual void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { } - /// + /// + /// Called when a COLR color layer ends. Subclasses override this to + /// emit the layer as a drawing operation. + /// protected virtual void EndLayer() { } + /// + /// Returns the set of text decorations enabled for the current glyph. + /// The font engine calls this to decide which SetDecoration callbacks to emit. + /// Subclasses override this to include decorations implied by rich-text pens + /// (e.g. ). + /// + /// A flags enum of the active text decorations. public virtual TextDecorations EnabledDecorations() => this.parameters.TextRun.TextDecorations; - /// + /// + /// Override point for subclasses to emit decoration drawing operations. + /// Called after the base class has built and registered the decoration path + /// in . + /// + /// The type of decoration (underline, strikeout, or overline). + /// The start position of the decoration line. + /// The end position of the decoration line. + /// The thickness of the decoration line in pixels. public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { } + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Point ClampToPixel(PointF point) => Point.Truncate(point); + /// + /// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and + /// orientation into account. Even-thickness lines snap to whole pixels; odd-thickness + /// lines snap to half pixels so the stroke center lands on a pixel boundary. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static PointF ClampToPixel(PointF point, int thickness, bool rotated) { - // Even. Clamp to whole pixels. + // Even thickness: snap to whole pixels. if ((thickness & 1) == 0) { return Point.Truncate(point); } - // Odd. Clamp to half pixels. + // Odd thickness: snap to half pixels along the perpendicular axis + // so the 1px-wide center row/column aligns with physical pixels. if (rotated) { return Point.Truncate(point) + new Vector2(.5F, 0); @@ -446,12 +562,19 @@ private static PointF ClampToPixel(PointF point, int thickness, bool rotated) return Point.Truncate(point) + new Vector2(0, .5F); } + /// + /// Records the start, end, and thickness of a previously emitted decoration line + /// so that the next adjacent decoration can be stitched seamlessly. + /// private struct TextDecorationDetails { + /// Gets or sets the start position of the decoration. public Vector2 Start { get; set; } + /// Gets or sets the end position of the decoration. public Vector2 End { get; set; } + /// Gets or sets the decoration thickness in pixels. public float Thickness { get; internal set; } } } diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs index 5f317f2ed..378591ee3 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs @@ -6,7 +6,9 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// -/// rendering surface that Fonts can use to generate Shapes. +/// A rendering surface that Fonts can use to generate shapes. +/// Extends by adding a configurable origin offset +/// so that all captured geometry is translated by the specified amount. /// internal class GlyphBuilder : BaseGlyphBuilder { diff --git a/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs index 706b772ac..2c843e2ed 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs @@ -10,9 +10,16 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// /// A rendering surface that Fonts can use to generate shapes by following a path. +/// Each glyph is positioned along the path and rotated to match the path tangent +/// at the glyph's horizontal center. /// internal sealed class PathGlyphBuilder : GlyphBuilder { + /// + /// The path that glyphs are laid out along. Exposed as + /// to access the method for efficient + /// position + tangent queries. + /// private readonly IPathInternals path; /// @@ -27,23 +34,35 @@ public PathGlyphBuilder(IPath path) } else { + // Wrap in ComplexPolygon to gain IPathInternals. this.path = new ComplexPolygon(path); } } /// - protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - => this.TransformGlyph(in bounds); + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // Translate + rotate the glyph to follow the path. Always returns true because + // path-based glyphs are never cached (each has a unique per-position transform). + this.TransformGlyph(in bounds); + return true; + } + /// + /// Computes the translation + rotation matrix that places a glyph along the path. + /// The glyph's horizontal center is mapped to the path distance, and the glyph + /// is rotated to match the path tangent at that point. + /// + /// The font-metric bounding rectangle of the glyph. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void TransformGlyph(in FontRectangle bounds) { - // Find the point of this intersection along the given path. - // We want to find the point on the path that is closest to the center-bottom side of the glyph. + // Query the path at the glyph's horizontal center. Vector2 half = new(bounds.Width * .5F, 0); SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); - // Now offset to our target point since we're aligning the top-left location of our glyph against the path. + // Translate so the glyph's top-left aligns with the path point, + // then rotate around the path point to follow the tangent. Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); Matrix3x2 matrix = Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); From bdf15aa7e08d0f6c031fc2d2847a093be5ebd911 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Feb 2026 01:30:49 +1000 Subject: [PATCH 017/136] Add WebGPU brush composers and pipeline infra --- .../Brushes/IWebGPUBrushComposer.cs | 44 +++ .../Brushes/WebGPUBrushComposerFactory.cs | 49 +++ .../Brushes/WebGPUBrushData.cs | 39 -- .../WebGPUImageBrushComposer{TPixel}.cs | 184 +++++++++ .../Brushes/WebGPUSolidBrushComposer.cs | 125 ++++++ .../Shaders/ImageBrushCompositeShader.cs | 121 ++++++ ...Shader.cs => SolidBrushCompositeShader.cs} | 60 ++- .../WebGPUCompositeBindGroupLayoutFactory.cs | 20 + .../WebGPUCompositeInstanceData.cs | 33 +- .../WebGPUDrawingBackend.cs | 161 ++++---- .../WebGPUFlushContext.cs | 365 +++++++++++++----- .../WebGPURasterizer.cs | 2 +- .../Backends/DefaultDrawingBackend.cs | 5 + .../Processing/Backends/IDrawingBackend.cs | 11 + .../DrawingCanvasBatcher{TPixel}.cs | 17 +- .../Processing/DrawingCanvas{TPixel}.cs | 353 ++++++++++++++++- .../Processing/ImageBrush.cs | 39 +- .../Backends/SkiaCoverageDrawingBackend.cs | 4 + .../Backends/WebGPUDrawingBackendTests.cs | 213 ++++++---- .../Processing/DrawingCanvasBatcherTests.cs | 34 ++ .../Processing/DrawingCanvasDrawImageTests.cs | 52 +++ .../RasterizerDefaultsExtensionsTests.cs | 4 + 22 files changed, 1562 insertions(+), 373 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs rename src/ImageSharp.Drawing.WebGPU/Shaders/{CompositeCoverageShader.cs => SolidBrushCompositeShader.cs} (68%) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs new file mode 100644 index 000000000..51c09f561 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// Defines brush-specific GPU composition behavior. +/// +internal unsafe interface IWebGPUBrushComposer +{ + /// + /// Gets or creates the render pipeline required by this brush composer. + /// + /// The active WebGPU flush context. + /// The created or cached render pipeline. + /// The error message when pipeline acquisition fails. + /// if the pipeline is available; otherwise . + public bool TryGetOrCreatePipeline( + WebGPUFlushContext flushContext, + out RenderPipeline* pipeline, + out string? error); + + /// + /// Populates brush-specific fields in the shared composite instance payload. + /// + /// The instance payload to update. + public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance); + + /// + /// Creates the bind group for this brush using the current coverage and instance buffers. + /// + /// The active WebGPU flush context. + /// The coverage texture view for the current batch. + /// The instance buffer offset. + /// The bound instance byte length. + /// The created bind group. + public BindGroup* CreateBindGroup( + WebGPUFlushContext flushContext, + TextureView* coverageView, + nuint instanceOffset, + nuint instanceBytes); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs new file mode 100644 index 000000000..aedc93b35 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// Creates brush composers for WebGPU composition commands. +/// +internal static class WebGPUBrushComposerFactory +{ + /// + /// Returns whether WebGPU can compose directly. + /// + public static bool IsSupportedBrush(Brush brush) + { + if (brush is SolidBrush) + { + return true; + } + + return brush is ImageBrush; + } + + /// + /// Creates a brush composer for the given prepared command. + /// + /// The brush composer. + public static IWebGPUBrushComposer Create( + WebGPUFlushContext flushContext, + in PreparedCompositionCommand command) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(command.Brush, nameof(command.Brush)); + + if (command.Brush is SolidBrush solidBrush) + { + return new WebGPUSolidBrushComposer(solidBrush); + } + + if (command.Brush is ImageBrush imageBrush) + { + return WebGPUImageBrushComposer.Create(flushContext, imageBrush, command.BrushBounds); + } + + throw new InvalidOperationException($"Unexpected brush type '{command.Brush.GetType().FullName}'."); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs deleted file mode 100644 index d1af7ad20..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushData.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal enum WebGPUBrushKind : uint -{ - SolidColor = 0 -} - -internal readonly struct WebGPUBrushData -{ - public WebGPUBrushData(WebGPUBrushKind kind, Vector4 solidColor) - { - this.Kind = kind; - this.SolidColor = solidColor; - } - - public WebGPUBrushKind Kind { get; } - - public Vector4 SolidColor { get; } - - public static bool TryCreate(Brush brush, Rectangle brushBounds, out WebGPUBrushData brushData) - { - Guard.NotNull(brush, nameof(brush)); - _ = brushBounds; - - if (brush is SolidBrush solidBrush) - { - brushData = new WebGPUBrushData(WebGPUBrushKind.SolidColor, solidBrush.Color.ToScaledVector4()); - return true; - } - - brushData = default; - return false; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs new file mode 100644 index 000000000..b602ecc33 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs @@ -0,0 +1,184 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// GPU brush composer for image brushes. +/// +/// The pixel type used by the target composition surface. +internal sealed unsafe class WebGPUImageBrushComposer : IWebGPUBrushComposer + where TPixel : unmanaged, IPixel +{ + private const string PipelineKey = "image-brush"; + private readonly TextureView* sourceTextureView; + private readonly Rectangle sourceRegion; + private readonly int imageBrushOriginX; + private readonly int imageBrushOriginY; + private BindGroupLayout* bindGroupLayout; + + private WebGPUImageBrushComposer( + TextureView* sourceTextureView, + in Rectangle sourceRegion, + int imageBrushOriginX, + int imageBrushOriginY) + { + this.sourceTextureView = sourceTextureView; + this.sourceRegion = sourceRegion; + this.imageBrushOriginX = imageBrushOriginX; + this.imageBrushOriginY = imageBrushOriginY; + } + + /// + public bool TryGetOrCreatePipeline( + WebGPUFlushContext flushContext, + out RenderPipeline* pipeline, + out string? error) + => flushContext.DeviceState.TryGetOrCreateCompositePipeline( + PipelineKey, + ImageBrushCompositeShader.Code, + TryCreateBindGroupLayout, + flushContext.TextureFormat, + out this.bindGroupLayout, + out pipeline, + out error); + + /// + /// Creates a composer for one image brush command. + /// + public static WebGPUImageBrushComposer Create( + WebGPUFlushContext flushContext, + ImageBrush imageBrush, + Rectangle brushBounds) + { + Guard.NotNull(flushContext, nameof(flushContext)); + Guard.NotNull(imageBrush, nameof(imageBrush)); + + // Invariant: image brushes have already been normalized for the target TPixel path. + Image sourceImage = (Image)imageBrush.SourceImage; + + Rectangle sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); + if (!flushContext.TryGetOrCreateSourceTextureView(sourceImage, out TextureView* sourceView)) + { + throw new InvalidOperationException("Failed to acquire source texture view for image brush composition."); + } + + int imageBrushOriginX = checked(brushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); + int imageBrushOriginY = checked(brushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); + return new WebGPUImageBrushComposer(sourceView, in sourceRegion, imageBrushOriginX, imageBrushOriginY); + } + + /// + public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance) + { + instance.ImageRegionX = this.sourceRegion.X; + instance.ImageRegionY = this.sourceRegion.Y; + instance.ImageRegionWidth = this.sourceRegion.Width; + instance.ImageRegionHeight = this.sourceRegion.Height; + instance.ImageBrushOriginX = this.imageBrushOriginX; + instance.ImageBrushOriginY = this.imageBrushOriginY; + } + + /// + public BindGroup* CreateBindGroup( + WebGPUFlushContext flushContext, + TextureView* coverageView, + nuint instanceOffset, + nuint instanceBytes) + { + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[3]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageView + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = flushContext.InstanceBuffer, + Offset = instanceOffset, + Size = instanceBytes + }; + bindGroupEntries[2] = new BindGroupEntry + { + Binding = 2, + TextureView = this.sourceTextureView + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = this.bindGroupLayout, + EntryCount = 3, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + throw new InvalidOperationException("Failed to create image brush bind group."); + } + + return bindGroup; + } + + private static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[3]; + layoutEntries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + layoutEntries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Vertex | ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + layoutEntries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Fragment, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + BindGroupLayoutDescriptor layoutDescriptor = new() + { + EntryCount = 3, + Entries = layoutEntries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in layoutDescriptor); + if (layout is null) + { + error = "Failed to create image composite bind group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs new file mode 100644 index 000000000..bb1a651a3 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// GPU brush composer for solid-color brushes. +/// +internal sealed unsafe class WebGPUSolidBrushComposer : IWebGPUBrushComposer +{ + private const string PipelineKey = "solid-brush"; + private readonly Vector4 color; + private BindGroupLayout* bindGroupLayout; + + public WebGPUSolidBrushComposer(SolidBrush brush) + { + Guard.NotNull(brush, nameof(brush)); + this.color = brush.Color.ToScaledVector4(); + } + + /// + public bool TryGetOrCreatePipeline( + WebGPUFlushContext flushContext, + out RenderPipeline* pipeline, + out string? error) + => flushContext.DeviceState.TryGetOrCreateCompositePipeline( + PipelineKey, + SolidBrushCompositeShader.Code, + TryCreateBindGroupLayout, + flushContext.TextureFormat, + out this.bindGroupLayout, + out pipeline, + out error); + + /// + public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance) + => instance.SolidBrushColor = this.color; + + /// + public BindGroup* CreateBindGroup( + WebGPUFlushContext flushContext, + TextureView* coverageView, + nuint instanceOffset, + nuint instanceBytes) + { + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageView + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = flushContext.InstanceBuffer, + Offset = instanceOffset, + Size = instanceBytes + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = this.bindGroupLayout, + EntryCount = 2, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + throw new InvalidOperationException("Failed to create solid brush bind group."); + } + + return bindGroup; + } + + private static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; + layoutEntries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + layoutEntries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Vertex | ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + + BindGroupLayoutDescriptor layoutDescriptor = new() + { + EntryCount = 2, + Entries = layoutEntries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in layoutDescriptor); + if (layout is null) + { + error = "Failed to create solid composite bind group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs new file mode 100644 index 000000000..aeb09d7d4 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class ImageBrushCompositeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct CompositeInstanceData { + source_offset_x: i32, + source_offset_y: i32, + destination_x: i32, + destination_y: i32, + destination_width: i32, + destination_height: i32, + target_width: i32, + target_height: i32, + + image_region_x: i32, + image_region_y: i32, + image_region_width: i32, + image_region_height: i32, + image_brush_origin_x: i32, + image_brush_origin_y: i32, + _pad0: i32, + _pad1: i32, + + solid_brush_color: vec4, + blend_data: vec4, + }; + + @group(0) @binding(0) + var coverage: texture_2d; + + @group(0) @binding(1) + var instances: array; + + @group(0) @binding(2) + var source_image: texture_2d; + + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) local: vec2, + @location(1) @interpolate(flat) instance_index: u32, + }; + + @vertex + fn vs_main( + @builtin(vertex_index) vertex_index: u32, + @builtin(instance_index) instance_index: u32) -> VertexOutput { + let params = instances[instance_index]; + var vertices = array, 6>( + vec2(0.0, 0.0), + vec2(f32(params.destination_width), 0.0), + vec2(0.0, f32(params.destination_height)), + vec2(0.0, f32(params.destination_height)), + vec2(f32(params.destination_width), 0.0), + vec2(f32(params.destination_width), f32(params.destination_height))); + + let local = vertices[vertex_index]; + let pixel = vec2(f32(params.destination_x), f32(params.destination_y)) + local; + let ndc_x = (pixel.x / f32(params.target_width)) * 2.0 - 1.0; + let ndc_y = 1.0 - (pixel.y / f32(params.target_height)) * 2.0; + + var output: VertexOutput; + output.position = vec4(ndc_x, ndc_y, 0.0, 1.0); + output.local = local; + output.instance_index = instance_index; + return output; + } + + fn positive_mod(value: i32, divisor: i32) -> i32 { + return ((value % divisor) + divisor) % divisor; + } + + fn sample_brush(params: CompositeInstanceData, local: vec2) -> vec4 { + let local_x = i32(floor(local.x)); + let local_y = i32(floor(local.y)); + let destination_x = params.destination_x + local_x; + let destination_y = params.destination_y + local_y; + + let source_x = positive_mod(destination_x - params.image_brush_origin_x, params.image_region_width) + params.image_region_x; + let source_y = positive_mod(destination_y - params.image_brush_origin_y, params.image_region_height) + params.image_region_y; + + return textureLoad(source_image, vec2(source_x, source_y), 0); + } + + @fragment + fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let params = instances[input.instance_index]; + let local_x = i32(floor(input.local.x)); + let local_y = i32(floor(input.local.y)); + let source = vec2( + params.source_offset_x + local_x, + params.source_offset_y + local_y); + + let coverage_value = textureLoad(coverage, source, 0).r; + if (coverage_value <= 0.0) { + discard; + } + + let brush = sample_brush(params, input.local); + if (brush.a <= 0.0) { + discard; + } + + let source_alpha = brush.a * coverage_value * params.blend_data.x; + if (source_alpha <= 0.0) { + discard; + } + + return vec4(brush.rgb * source_alpha, source_alpha); + } + """u8, + .. "\0"u8 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs similarity index 68% rename from src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs index af450357d..332ad8941 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeCoverageShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs @@ -3,31 +3,32 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -internal static class CompositeCoverageShader +internal static class SolidBrushCompositeShader { private static readonly byte[] CodeBytes = [ .. """ struct CompositeInstanceData { - source_offset_x: u32, - source_offset_y: u32, - destination_x: u32, - destination_y: u32, - destination_width: u32, - destination_height: u32, - target_width: u32, - target_height: u32, - - brush_kind: u32, - _pad0: u32, - _pad1: u32, - _pad2: u32, + source_offset_x: i32, + source_offset_y: i32, + destination_x: i32, + destination_y: i32, + destination_width: i32, + destination_height: i32, + target_width: i32, + target_height: i32, + + image_region_x: i32, + image_region_y: i32, + image_region_width: i32, + image_region_height: i32, + image_brush_origin_x: i32, + image_brush_origin_y: i32, + _pad0: i32, + _pad1: i32, solid_brush_color: vec4, - blend_percentage: f32, - _pad3: f32, - _pad4: f32, - _pad5: f32, + blend_data: vec4, }; @group(0) @binding(0) @@ -67,37 +68,26 @@ fn vs_main( return output; } - fn sample_brush(params: CompositeInstanceData, _local: vec2) -> vec4 { - switch params.brush_kind { - case 0u: { - return params.solid_brush_color; - } - default: { - return vec4(0.0); - } - } - } - @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { let params = instances[input.instance_index]; - let local_x = u32(floor(input.local.x)); - let local_y = u32(floor(input.local.y)); + let local_x = i32(floor(input.local.x)); + let local_y = i32(floor(input.local.y)); let source = vec2( - i32(params.source_offset_x + local_x), - i32(params.source_offset_y + local_y)); + params.source_offset_x + local_x, + params.source_offset_y + local_y); let coverage_value = textureLoad(coverage, source, 0).r; if (coverage_value <= 0.0) { discard; } - let brush = sample_brush(params, input.local); + let brush = params.solid_brush_color; if (brush.a <= 0.0) { discard; } - let source_alpha = brush.a * coverage_value * params.blend_percentage; + let source_alpha = brush.a * coverage_value * params.blend_data.x; if (source_alpha <= 0.0) { discard; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs new file mode 100644 index 000000000..de5b34edd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Creates a bind-group layout for one composite brush pipeline. +/// +/// The WebGPU API facade. +/// The device used to create resources. +/// The created bind-group layout. +/// The error message when creation fails. +/// if the layout was created; otherwise . +internal unsafe delegate bool WebGPUCompositeBindGroupLayoutFactory( + WebGPU api, + Device* device, + out BindGroupLayout* bindGroupLayout, + out string? error); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs index faac7974c..e8d410f2b 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs @@ -9,21 +9,22 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; [StructLayout(LayoutKind.Sequential)] internal struct WebGPUCompositeInstanceData { - public uint SourceOffsetX; - public uint SourceOffsetY; - public uint DestinationX; - public uint DestinationY; - public uint DestinationWidth; - public uint DestinationHeight; - public uint TargetWidth; - public uint TargetHeight; - public uint BrushKind; - public uint Padding0; - public uint Padding1; - public uint Padding2; + public int SourceOffsetX; + public int SourceOffsetY; + public int DestinationX; + public int DestinationY; + public int DestinationWidth; + public int DestinationHeight; + public int TargetWidth; + public int TargetHeight; + public int ImageRegionX; + public int ImageRegionY; + public int ImageRegionWidth; + public int ImageRegionHeight; + public int ImageBrushOriginX; + public int ImageBrushOriginY; + public int Padding0; + public int Padding1; public Vector4 SolidBrushColor; - public float BlendPercentage; - public float Padding3; - public float Padding4; - public float Padding5; + public Vector4 BlendData; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 3976993ba..651dc12af 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -3,10 +3,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -123,6 +125,14 @@ public void FillPath( target.Bounds.Location)); } + /// + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + return WebGPUBrushComposerFactory.IsSupportedBrush(brush); + } + /// public void FlushCompositions( Configuration configuration, @@ -142,6 +152,21 @@ public void FlushCompositions( this.TestingCompositeCoverageCallCount += commandCount; bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); + if (!AreAllCompositionBrushesSupported(compositionBatch.Commands)) + { + if (compositionBatch.FlushId != 0) + { + throw new InvalidOperationException( + "Unsupported brush reached a shared WebGPU flush session. " + + "Flush-time brush support validation should have prevented this."); + } + + this.TestingFallbackPrepareCoverageCallCount++; + this.TestingFallbackCompositeCoverageCallCount += commandCount; + this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); + return; + } + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) { this.TestingFallbackPrepareCoverageCallCount++; @@ -169,15 +194,14 @@ public void FlushCompositions( : WebGPUFlushContext.Create(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); CompositionCoverageDefinition definition = compositionBatch.Definition; - if (TryPrepareGpuResources( + if (TryPrepareGpuCoverage( flushContext, in definition, - out RenderPipeline* pipeline, out WebGPUFlushContext.CoverageEntry? coverageEntry, out failure)) { gpuReady = true; - gpuSuccess = this.TryCompositeBatch(flushContext, pipeline, coverageEntry, target.Bounds, compositionBatch.Commands); + gpuSuccess = this.TryCompositeBatch(flushContext, coverageEntry, target.Bounds, compositionBatch.Commands, out failure); if (gpuSuccess) { if (useFlushSession && !compositionBatch.IsFinalBatchInFlush) @@ -247,6 +271,21 @@ public void FlushCompositions( this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) + where TPixel : unmanaged, IPixel + { + for (int i = 0; i < commands.Count; i++) + { + if (!WebGPUBrushComposerFactory.IsSupportedBrush(commands[i].Brush)) + { + return false; + } + } + + return true; + } + private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, @@ -277,24 +316,14 @@ private void FlushCompositionsFallback( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryPrepareGpuResources( + private static bool TryPrepareGpuCoverage( WebGPUFlushContext flushContext, in CompositionCoverageDefinition definition, - out RenderPipeline* pipeline, [NotNullWhen(true)] out WebGPUFlushContext.CoverageEntry? coverageEntry, out string? error) { lock (flushContext.DeviceState.SyncRoot) { - if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline( - flushContext.TextureFormat, - out pipeline, - out error)) - { - coverageEntry = null; - return false; - } - return flushContext.DeviceState.TryGetOrCreateCoverageEntry( in definition, flushContext.Queue, @@ -303,19 +332,27 @@ private static bool TryPrepareGpuResources( } } - private bool TryCompositeBatch( + private bool TryCompositeBatch( WebGPUFlushContext flushContext, - RenderPipeline* pipeline, WebGPUFlushContext.CoverageEntry coverageEntry, in Rectangle destinationBounds, - IReadOnlyList commands) + IReadOnlyList commands, + out string? error) + where TPixel : unmanaged, IPixel { + error = null; int commandCount = commands.Count; if (commandCount == 0) { return true; } + IWebGPUBrushComposer[] composers = new IWebGPUBrushComposer[commandCount]; + for (int i = 0; i < commandCount; i++) + { + composers[i] = WebGPUBrushComposerFactory.Create(flushContext, commands[i]); + } + nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); nuint instanceOffset = flushContext.InstanceBufferWriteOffset; nuint requiredCapacity = checked(instanceOffset + instanceBytes); @@ -339,6 +376,7 @@ private bool TryCompositeBatch( !flushContext.EnsureCommandEncoder() || !flushContext.BeginRenderPass()) { + error = "Failed to allocate WebGPU composition buffers or begin render pass."; return false; } @@ -348,28 +386,23 @@ private bool TryCompositeBatch( for (int i = 0; i < commandCount; i++) { PreparedCompositionCommand command = commands[i]; - if (!WebGPUBrushData.TryCreate(command.Brush, command.BrushBounds, out WebGPUBrushData brushData)) - { - return false; - } - int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; instances[i] = new WebGPUCompositeInstanceData { - SourceOffsetX = (uint)command.SourceOffset.X, - SourceOffsetY = (uint)command.SourceOffset.Y, - DestinationX = (uint)destinationX, - DestinationY = (uint)destinationY, - DestinationWidth = (uint)command.DestinationRegion.Width, - DestinationHeight = (uint)command.DestinationRegion.Height, - TargetWidth = (uint)targetWidth, - TargetHeight = (uint)targetHeight, - BrushKind = (uint)brushData.Kind, - SolidBrushColor = brushData.SolidColor, - BlendPercentage = command.GraphicsOptions.BlendPercentage + SourceOffsetX = command.SourceOffset.X, + SourceOffsetY = command.SourceOffset.Y, + DestinationX = destinationX, + DestinationY = destinationY, + DestinationWidth = command.DestinationRegion.Width, + DestinationHeight = command.DestinationRegion.Height, + TargetWidth = targetWidth, + TargetHeight = targetHeight, + BlendData = new Vector4(command.GraphicsOptions.BlendPercentage, 0, 0, 0) }; + + composers[i].PopulateInstanceData(ref instances[i]); } fixed (WebGPUCompositeInstanceData* instancesPtr = instances) @@ -378,57 +411,31 @@ private bool TryCompositeBatch( flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, instanceOffset, instancesPtr, instanceBytes); } - BindGroup* bindGroup = this.CreateCoverageBindGroup(flushContext, coverageEntry, instanceOffset, instanceBytes); - if (bindGroup is null) + for (int i = 0; i < commandCount; i++) { - return false; - } + IWebGPUBrushComposer composer = composers[i]; - flushContext.TrackBindGroup(bindGroup); - flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); - flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); - flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, (uint)commandCount, 0, 0); - - flushContext.AdvanceInstanceBufferOffset(instanceOffset + instanceBytes); + if (!composer.TryGetOrCreatePipeline(flushContext, out RenderPipeline* pipeline, out string? pipelineError)) + { + error = pipelineError ?? "Failed to create composite pipeline."; + return false; + } - return true; - } + BindGroup* bindGroup = composer.CreateBindGroup( + flushContext, + coverageEntry.GPUCoverageView, + instanceOffset, + instanceBytes); - private BindGroup* CreateCoverageBindGroup( - WebGPUFlushContext flushContext, - WebGPUFlushContext.CoverageEntry coverageEntry, - nuint instanceOffset, - nuint instanceBytes) - { - if (flushContext.DeviceState.CompositeBindGroupLayout is null || - coverageEntry.GPUCoverageView is null || - flushContext.InstanceBuffer is null) - { - return null; + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); + flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, (uint)i); } - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageEntry.GPUCoverageView - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = flushContext.InstanceBuffer, - Offset = instanceOffset, - Size = instanceBytes - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = flushContext.DeviceState.CompositeBindGroupLayout, - EntryCount = 2, - Entries = bindGroupEntries - }; + flushContext.AdvanceInstanceBufferOffset(instanceOffset + instanceBytes); - return flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + return true; } private bool TryFinalizeFlush( diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 1380a3603..15aa88508 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -31,6 +31,13 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable private bool ownsReadbackBuffer; private WebGPUCompositeInstanceData[]? compositeInstanceData; private readonly List transientBindGroups = []; + private readonly List transientTextureViews = []; + private readonly List transientTextures = []; + + // Flush-scoped source image cache: + // key = source Image reference, value = uploaded texture view handle. + // Handles are released when this flush context is disposed. + private readonly Dictionary cachedSourceTextureViews = new(ReferenceEqualityComparer.Instance); private WebGPUFlushContext( WebGPURuntime.Lease runtimeLease, @@ -89,7 +96,7 @@ public Span GetCompositeInstanceSpan(int count) { if (count <= 0) { - return Span.Empty; + return []; } WebGPUCompositeInstanceData[]? cached = this.compositeInstanceData; @@ -163,7 +170,7 @@ public static WebGPUFlushContext CreateUploadContext(ICanvasFrame + /// Tracks a transient texture view allocated during this flush. + /// + public void TrackTextureView(TextureView* textureView) + { + if (textureView is not null) + { + this.transientTextureViews.Add((nint)textureView); + } + } + + /// + /// Tracks a transient texture allocated during this flush. + /// + public void TrackTexture(Texture* texture) + { + if (texture is not null) + { + this.transientTextures.Add((nint)texture); + } + } + + /// + /// Gets a texture view for the source image from this flush cache, creating and uploading it on first use. + /// + internal bool TryGetOrCreateSourceTextureView(Image sourceImage, out TextureView* textureView) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(sourceImage, nameof(sourceImage)); + + if (this.cachedSourceTextureViews.TryGetValue(sourceImage, out nint cachedHandle) && cachedHandle != 0) + { + textureView = (TextureView*)cachedHandle; + return true; + } + + return this.TryCreateAndCacheSourceTextureView(sourceImage, out textureView); + } + + /// + /// Uploads one source image into a transient GPU texture and stores the resulting view in the flush cache. + /// + private bool TryCreateAndCacheSourceTextureView(Image sourceImage, out TextureView* textureView) + where TPixel : unmanaged, IPixel + { + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)sourceImage.Width, (uint)sourceImage.Height, 1), + Format = this.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* sourceTexture = this.Api.DeviceCreateTexture(this.Device, in textureDescriptor); + if (sourceTexture is null) + { + textureView = null; + return false; + } + + TextureViewDescriptor sourceViewDescriptor = new() + { + Format = this.TextureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* sourceView = this.Api.TextureCreateView(sourceTexture, in sourceViewDescriptor); + if (sourceView is null) + { + this.Api.TextureRelease(sourceTexture); + textureView = null; + return false; + } + + Buffer2DRegion sourceRegionPixels = new(sourceImage.Frames.RootFrame.PixelBuffer, sourceImage.Bounds); + UploadTextureFromRegion(this.Api, this.Queue, sourceTexture, sourceRegionPixels); + + this.TrackTexture(sourceTexture); + this.TrackTextureView(sourceView); + this.cachedSourceTextureViews[sourceImage] = (nint)sourceView; + textureView = sourceView; + return true; + } + public void Dispose() { if (this.disposed) @@ -416,7 +514,22 @@ public void Dispose() this.Api.BindGroupRelease((BindGroup*)this.transientBindGroups[i]); } + for (int i = 0; i < this.transientTextureViews.Count; i++) + { + this.Api.TextureViewRelease((TextureView*)this.transientTextureViews[i]); + } + + for (int i = 0; i < this.transientTextures.Count; i++) + { + this.Api.TextureRelease((Texture*)this.transientTextures[i]); + } + this.transientBindGroups.Clear(); + this.transientTextureViews.Clear(); + this.transientTextures.Clear(); + + // Cache entries point to transient texture views that are released above. + this.cachedSourceTextureViews.Clear(); this.ReadbackBuffer = null; this.TargetView = null; @@ -850,10 +963,8 @@ public void AdvanceInstanceBufferOffset(nuint newOffset) internal sealed class DeviceSharedState : IDisposable { private readonly Dictionary coverageCache = []; - private readonly ConcurrentDictionary compositePipelines = new(); + private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); private WebGPURasterizer? coverageRasterizer; - private PipelineLayout* compositePipelineLayout; - private ShaderModule* compositeShaderModule; private bool disposed; internal DeviceSharedState(WebGPU api, Device* device) @@ -872,11 +983,9 @@ internal DeviceSharedState(WebGPU api, Device* device) public Device* Device { get; } - public BindGroupLayout* CompositeBindGroupLayout { get; private set; } - public int CoverageCount => this.coverageCache.Count; - public bool TryEnsureResources(out string? error) + public bool TryEnsureCoverageResources(out string? error) { if (this.disposed) { @@ -884,14 +993,6 @@ public bool TryEnsureResources(out string? error) return false; } - if (this.CompositeBindGroupLayout is null || this.compositePipelineLayout is null) - { - if (!this.TryCreateCompositeInfrastructure(out error)) - { - return false; - } - } - this.coverageRasterizer ??= new WebGPURasterizer(this.Api); if (!this.coverageRasterizer.IsInitialized && !this.coverageRasterizer.Initialize(this.Device)) { @@ -909,7 +1010,7 @@ public bool TryGetOrCreateCoverageEntry( [NotNullWhen(true)] out CoverageEntry? coverageEntry, out string? error) { - if (!this.TryEnsureResources(out error)) + if (!this.TryEnsureCoverageResources(out error)) { coverageEntry = null; return false; @@ -947,38 +1048,85 @@ public bool TryGetOrCreateCoverageEntry( return true; } - public bool TryGetOrCreateCompositePipeline(TextureFormat textureFormat, out RenderPipeline* pipeline, out string? error) + public bool TryGetOrCreateCompositePipeline( + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + TextureFormat textureFormat, + out BindGroupLayout* bindGroupLayout, + out RenderPipeline* pipeline, + out string? error) { - if (!this.TryEnsureResources(out error)) + bindGroupLayout = null; + pipeline = null; + + if (this.disposed) { - pipeline = null; + error = "WebGPU device state is disposed."; return false; } - if (this.compositePipelines.TryGetValue(textureFormat, out nint existingHandle) && existingHandle != 0) + if (string.IsNullOrWhiteSpace(pipelineKey)) { - pipeline = (RenderPipeline*)existingHandle; - return true; + error = "Composite pipeline key cannot be empty."; + return false; } - RenderPipeline* created = this.CreateCompositePipelineForFormat(textureFormat); - if (created is null) + if (shaderCode.IsEmpty) { - pipeline = null; - error = $"Failed to create composite pipeline for format '{textureFormat}'."; + error = $"Composite shader code is missing for pipeline '{pipelineKey}'."; return false; } - nint createdHandle = (nint)created; - nint cachedHandle = this.compositePipelines.GetOrAdd(textureFormat, createdHandle); - if (cachedHandle != createdHandle) + CompositePipelineInfrastructure infrastructure = this.compositePipelines.GetOrAdd( + pipelineKey, + static _ => new CompositePipelineInfrastructure()); + + lock (infrastructure) { - this.Api.RenderPipelineRelease(created); - } + if (infrastructure.BindGroupLayout is null || + infrastructure.PipelineLayout is null || + infrastructure.ShaderModule is null) + { + if (!this.TryCreateCompositeInfrastructure( + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* createdBindGroupLayout, + out PipelineLayout* createdPipelineLayout, + out ShaderModule* createdShaderModule, + out error)) + { + return false; + } - pipeline = (RenderPipeline*)cachedHandle; - error = null; - return true; + infrastructure.BindGroupLayout = createdBindGroupLayout; + infrastructure.PipelineLayout = createdPipelineLayout; + infrastructure.ShaderModule = createdShaderModule; + } + + bindGroupLayout = infrastructure.BindGroupLayout; + if (infrastructure.Pipelines.TryGetValue(textureFormat, out nint cachedPipelineHandle) && cachedPipelineHandle != 0) + { + pipeline = (RenderPipeline*)cachedPipelineHandle; + error = null; + return true; + } + + RenderPipeline* createdPipeline = this.CreateCompositePipeline( + infrastructure.PipelineLayout, + infrastructure.ShaderModule, + textureFormat); + if (createdPipeline is null) + { + error = $"Failed to create composite pipeline '{pipelineKey}' for format '{textureFormat}'."; + return false; + } + + infrastructure.Pipelines[textureFormat] = (nint)createdPipeline; + pipeline = createdPipeline; + error = null; + return true; + } } public void Dispose() @@ -998,92 +1146,49 @@ public void Dispose() this.coverageRasterizer?.Release(); this.coverageRasterizer = null; - foreach (KeyValuePair entry in this.compositePipelines) + foreach (CompositePipelineInfrastructure infrastructure in this.compositePipelines.Values) { - if (entry.Value != 0) - { - this.Api.RenderPipelineRelease((RenderPipeline*)entry.Value); - } + this.ReleaseCompositeInfrastructure(infrastructure); } this.compositePipelines.Clear(); - if (this.compositePipelineLayout is not null) - { - this.Api.PipelineLayoutRelease(this.compositePipelineLayout); - this.compositePipelineLayout = null; - } - - if (this.compositeShaderModule is not null) - { - this.Api.ShaderModuleRelease(this.compositeShaderModule); - this.compositeShaderModule = null; - } - - if (this.CompositeBindGroupLayout is not null) - { - this.Api.BindGroupLayoutRelease(this.CompositeBindGroupLayout); - this.CompositeBindGroupLayout = null; - } - this.disposed = true; } - private bool TryCreateCompositeInfrastructure(out string? error) + private bool TryCreateCompositeInfrastructure( + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out PipelineLayout* pipelineLayout, + out ShaderModule* shaderModule, + out string? error) { - BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[2]; - layoutEntries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Fragment, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - layoutEntries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Vertex | ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - - BindGroupLayoutDescriptor layoutDescriptor = new() - { - EntryCount = 2, - Entries = layoutEntries - }; + bindGroupLayout = null; + pipelineLayout = null; + shaderModule = null; - this.CompositeBindGroupLayout = this.Api.DeviceCreateBindGroupLayout(this.Device, in layoutDescriptor); - if (this.CompositeBindGroupLayout is null) + if (!bindGroupLayoutFactory(this.Api, this.Device, out bindGroupLayout, out error)) { - error = "Failed to create composite bind group layout."; return false; } BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; - bindGroupLayouts[0] = this.CompositeBindGroupLayout; + bindGroupLayouts[0] = bindGroupLayout; PipelineLayoutDescriptor pipelineLayoutDescriptor = new() { BindGroupLayoutCount = 1, BindGroupLayouts = bindGroupLayouts }; - this.compositePipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); - if (this.compositePipelineLayout is null) + pipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); + if (pipelineLayout is null) { + this.Api.BindGroupLayoutRelease(bindGroupLayout); error = "Failed to create composite pipeline layout."; return false; } - ReadOnlySpan shaderCode = CompositeCoverageShader.Code; fixed (byte* shaderCodePtr = shaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() @@ -1097,11 +1202,13 @@ private bool TryCreateCompositeInfrastructure(out string? error) NextInChain = (ChainedStruct*)&wgslDescriptor }; - this.compositeShaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); + shaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); } - if (this.compositeShaderModule is null) + if (shaderModule is null) { + this.Api.PipelineLayoutRelease(pipelineLayout); + this.Api.BindGroupLayoutRelease(bindGroupLayout); error = "Failed to create composite shader module."; return false; } @@ -1110,25 +1217,29 @@ private bool TryCreateCompositeInfrastructure(out string? error) return true; } - private RenderPipeline* CreateCompositePipelineForFormat(TextureFormat textureFormat) + private RenderPipeline* CreateCompositePipeline( + PipelineLayout* pipelineLayout, + ShaderModule* shaderModule, + TextureFormat textureFormat) { - if (this.compositePipelineLayout is null || this.compositeShaderModule is null) - { - return null; - } - ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; fixed (byte* vertexEntryPointPtr = vertexEntryPoint) { fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) { - return this.CreateCompositePipeline(this.compositeShaderModule, vertexEntryPointPtr, fragmentEntryPointPtr, textureFormat); + return this.CreateCompositePipelineCore( + pipelineLayout, + shaderModule, + vertexEntryPointPtr, + fragmentEntryPointPtr, + textureFormat); } } } - private RenderPipeline* CreateCompositePipeline( + private RenderPipeline* CreateCompositePipelineCore( + PipelineLayout* pipelineLayout, ShaderModule* shaderModule, byte* vertexEntryPointPtr, byte* fragmentEntryPointPtr, @@ -1176,7 +1287,7 @@ private bool TryCreateCompositeInfrastructure(out string? error) RenderPipelineDescriptor descriptor = new() { - Layout = this.compositePipelineLayout, + Layout = pipelineLayout, Vertex = vertexState, Primitive = new PrimitiveState { @@ -1198,6 +1309,37 @@ private bool TryCreateCompositeInfrastructure(out string? error) return this.Api.DeviceCreateRenderPipeline(this.Device, in descriptor); } + private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infrastructure) + { + foreach (nint pipelineHandle in infrastructure.Pipelines.Values) + { + if (pipelineHandle != 0) + { + this.Api.RenderPipelineRelease((RenderPipeline*)pipelineHandle); + } + } + + infrastructure.Pipelines.Clear(); + + if (infrastructure.PipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(infrastructure.PipelineLayout); + infrastructure.PipelineLayout = null; + } + + if (infrastructure.ShaderModule is not null) + { + this.Api.ShaderModuleRelease(infrastructure.ShaderModule); + infrastructure.ShaderModule = null; + } + + if (infrastructure.BindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(infrastructure.BindGroupLayout); + infrastructure.BindGroupLayout = null; + } + } + private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) { if (entry.GPUCoverageView is not null) @@ -1212,6 +1354,17 @@ private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) entry.GPUCoverageTexture = null; } } + + private sealed class CompositePipelineInfrastructure + { + public Dictionary Pipelines { get; } = []; + + public BindGroupLayout* BindGroupLayout { get; set; } + + public PipelineLayout* PipelineLayout { get; set; } + + public ShaderModule* ShaderModule { get; set; } + } } internal sealed class CoverageEntry diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs index 19a4d2485..5c496c129 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs @@ -799,7 +799,7 @@ private static bool TryBuildCoverageTriangles( return false; } - float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelBoundary ? 0.5F : 0F; + float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; float offsetX = sampleShift - interestLocation.X; float offsetY = sampleShift - interestLocation.Y; diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index c5e7038e4..cb1a29484 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -42,6 +42,11 @@ public static DefaultDrawingBackend Create(IRasterizer rasterizer) return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new DefaultDrawingBackend(rasterizer); } + /// + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel + => true; + /// public void FillPath( ICanvasFrame target, diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index a65c07254..c5f319d31 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -14,6 +14,17 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal interface IDrawingBackend { + /// + /// Determines whether the backend can compose the provided brush type directly for . + /// + /// The destination pixel format. + /// The brush used by a pending composition command. + /// + /// when the backend can compose the brush directly; otherwise . + /// + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel; + /// /// Fills a path into a destination target region. /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 17ee96fd5..b47b7c7ff 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -138,9 +138,20 @@ public void FlushCompositions() return; } - // All batches emitted by this call share one flush id so backends can keep - // transient per-flush GPU state and finalize once on the last batch. - int flushId = Interlocked.Increment(ref nextFlushId); + // Use one shared flush id only when all queued brushes are directly supported by + // the active backend. If any brush is unsupported, backends receive independent + // batches (flushId = 0) so they can route each batch safely without shared state. + bool supportsSharedFlush = true; + for (int i = 0; i < this.commands.Count; i++) + { + if (!this.backend.IsCompositionBrushSupported(this.commands[i].Brush)) + { + supportsSharedFlush = false; + break; + } + } + + int flushId = supportsSharedFlush ? Interlocked.Increment(ref nextFlushId) : 0; for (int i = 0; i < batches.Count; i++) { CompositionBatch batch = batches[i]; diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 1b3c811ed..245a9d7b0 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -18,10 +19,34 @@ namespace SixLabors.ImageSharp.Drawing.Processing; public sealed class DrawingCanvas : IDisposable where TPixel : unmanaged, IPixel { + /// + /// Processing configuration used by operations executed through this canvas. + /// private readonly Configuration configuration; + + /// + /// Backend responsible for rasterizing and composing draw commands. + /// private readonly IDrawingBackend backend; + + /// + /// Destination frame receiving rendered output. + /// private readonly ICanvasFrame targetFrame; + + /// + /// Command batcher used to defer and submit composition commands. + /// private readonly DrawingCanvasBatcher batcher; + + /// + /// Temporary image resources that must stay alive until queued commands are flushed. + /// + private readonly List> pendingImageResources = []; + + /// + /// Tracks whether this instance has already been disposed. + /// private bool isDisposed; /// @@ -44,6 +69,13 @@ public DrawingCanvas(Configuration configuration, ICanvasFrame targetFra { } + /// + /// Initializes a new instance of the class + /// with an explicit backend. + /// + /// The active processing configuration. + /// The drawing backend implementation. + /// The destination frame. internal DrawingCanvas( Configuration configuration, IDrawingBackend backend, @@ -56,6 +88,14 @@ internal DrawingCanvas( { } + /// + /// Initializes a new instance of the class + /// with explicit backend and batcher instances. + /// + /// The active processing configuration. + /// The drawing backend implementation. + /// The destination frame. + /// The command batcher used for deferred composition. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, @@ -147,6 +187,13 @@ public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOp public void FillPath(IPath path, Brush brush, DrawingOptions options) => this.FillPath(path, brush, options, RasterizerSamplingOrigin.PixelBoundary); + /// + /// Fills a path in local coordinates using an explicit rasterizer sampling origin. + /// + /// The path to fill. + /// Brush used to shade covered pixels. + /// Drawing options for fill and rasterization behavior. + /// Sampling origin used by the rasterizer. internal void FillPath( IPath path, Brush brush, @@ -252,6 +299,124 @@ public void DrawText( this.DrawTextOperations(textRenderer.DrawingOperations, drawingOptions); } + /// + /// Draws an image source region into a destination rectangle. + /// + /// The source image. + /// The source rectangle within . + /// The destination rectangle in local canvas coordinates. + /// Drawing options defining blend and transform behavior. + /// + /// Optional resampler used when scaling or transforming the image. Defaults to . + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + DrawingOptions drawingOptions, + IResampler? sampler = null) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + Guard.NotNull(drawingOptions, nameof(drawingOptions)); + + if (sourceRect.Width <= 0 || + sourceRect.Height <= 0 || + destinationRect.Width <= 0 || + destinationRect.Height <= 0) + { + return; + } + + Rectangle clippedSourceRect = Rectangle.Intersect(sourceRect, image.Bounds); + if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0) + { + return; + } + + RectangleF clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect); + if (clippedDestinationRect.Width <= 0 || clippedDestinationRect.Height <= 0) + { + return; + } + + Size scaledSize = new( + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)), + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height))); + + bool requiresScaling = + clippedSourceRect.Width != scaledSize.Width || + clippedSourceRect.Height != scaledSize.Height; + + Image brushImage = image; + RectangleF brushImageRegion = clippedSourceRect; + RectangleF renderDestinationRect = clippedDestinationRect; + Image? ownedImage = null; + + try + { + // Phase 1: Prepare source pixels (crop/scale) in image-local space. + if (requiresScaling) + { + ownedImage = CreateScaledDrawImage(image, clippedSourceRect, scaledSize, sampler); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + else if (clippedSourceRect != image.Bounds) + { + ownedImage = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + + // Phase 2: Apply canvas transform to image content when requested. + if (drawingOptions.Transform != Matrix3x2.Identity) + { + Image transformed = CreateTransformedDrawImage( + brushImage, + clippedDestinationRect, + drawingOptions.Transform, + sampler, + out renderDestinationRect); + + ownedImage?.Dispose(); + ownedImage = transformed; + brushImage = transformed; + brushImageRegion = transformed.Bounds; + } + + if (renderDestinationRect.Width <= 0 || renderDestinationRect.Height <= 0) + { + return; + } + + // Phase 3: Transfer temp-image ownership to deferred batch execution. + if (!ReferenceEquals(brushImage, image)) + { + this.pendingImageResources.Add(brushImage); + ownedImage = null; + } + + ImageBrush brush = new(brushImage, brushImageRegion); + IPath destinationPath = new RectangularPolygon( + renderDestinationRect.X, + renderDestinationRect.Y, + renderDestinationRect.Width, + renderDestinationRect.Height); + + this.FillPath(destinationPath, brush, drawingOptions); + } + finally + { + ownedImage?.Dispose(); + } + } + + /// + /// Converts rendered text operations to composition commands and submits them to the batcher. + /// + /// Text drawing operations produced by glyph layout/rendering. + /// Drawing options applied to each operation. private void DrawTextOperations(List operations, DrawingOptions drawingOptions) { this.EnsureNotDisposed(); @@ -288,7 +453,14 @@ private void DrawTextOperations(List operations, DrawingOption public void Flush() { this.EnsureNotDisposed(); - this.batcher.FlushCompositions(); + try + { + this.batcher.FlushCompositions(); + } + finally + { + this.DisposePendingImageResources(); + } } /// @@ -299,13 +471,28 @@ public void Dispose() return; } - this.batcher.FlushCompositions(); - this.isDisposed = true; + try + { + this.batcher.FlushCompositions(); + } + finally + { + this.DisposePendingImageResources(); + this.isDisposed = true; + } } + /// + /// Ensures this instance is not disposed. + /// private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + /// + /// Normalizes text options to avoid applying origin translation twice when path-based text is used. + /// + /// Input text options. + /// Normalized text options for rendering. private static RichTextOptions ConfigureTextOptions(RichTextOptions options) { if (options.Path is not null && options.Origin != Vector2.Zero) @@ -322,6 +509,13 @@ private static RichTextOptions ConfigureTextOptions(RichTextOptions options) return options; } + /// + /// Builds a normalized composition command for a text drawing operation. + /// + /// The source drawing operation. + /// Drawing options applied to the operation. + /// Optional cache used to reuse definition key computations. + /// A composition command ready for batching. private CompositionCommand CreateCompositionCommand( DrawingOperation operation, DrawingOptions drawingOptions, @@ -392,4 +586,157 @@ private CompositionCommand CreateCompositionCommand( destinationOffset, definitionKeyCache); } + + /// + /// Creates resize options used for image drawing operations. + /// + /// Requested output size. + /// Optional resampler. Defaults to bicubic. + /// A resize options instance configured for stretch behavior. + private static ResizeOptions CreateDrawImageResizeOptions(Size size, IResampler? sampler) + => new() + { + Size = size, + Mode = ResizeMode.Stretch, + Sampler = sampler ?? KnownResamplers.Bicubic + }; + + /// + /// Creates a scaled image for drawing, optionally cropping to a source region first. + /// + /// The source image. + /// The clipped source rectangle. + /// The target scaled size. + /// Optional resampler used for scaling. + /// A new image containing the scaled pixels. + private static Image CreateScaledDrawImage( + Image image, + Rectangle clippedSourceRect, + Size scaledSize, + IResampler? sampler) + { + ResizeOptions effectiveResizeOptions = CreateDrawImageResizeOptions(scaledSize, sampler); + if (clippedSourceRect == image.Bounds) + { + return image.Clone(ctx => ctx.Resize(effectiveResizeOptions)); + } + + Image result = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + result.Mutate(ctx => ctx.Resize(effectiveResizeOptions)); + return result; + } + + /// + /// Applies a transform to image content and returns the transformed image. + /// + /// The source image. + /// Destination rectangle in canvas coordinates. + /// Canvas transform to apply. + /// Optional resampler used during transform. + /// Receives the transformed destination bounds. + /// A new image containing transformed pixels. + private static Image CreateTransformedDrawImage( + Image image, + RectangleF destinationRect, + Matrix3x2 transform, + IResampler? sampler, + out RectangleF transformedDestinationRect) + { + // Source space: pixel coordinates in the untransformed source image (0..Width, 0..Height). + // Destination space: where that image would land on the canvas without any extra transform. + // This matrix maps source -> destination by scaling to destination size then translating to destination origin. + Matrix3x2 sourceToDestination = Matrix3x2.CreateScale( + destinationRect.Width / image.Width, + destinationRect.Height / image.Height) + * Matrix3x2.CreateTranslation(destinationRect.X, destinationRect.Y); + + // Apply the canvas transform after source->destination placement: + // source -> destination -> transformed-canvas. + Matrix3x2 sourceToTransformedCanvas = sourceToDestination * transform; + + // Compute the transformed axis-aligned bounds so we know how large the output bitmap must be. + transformedDestinationRect = TransformRectangle( + new RectangleF(0, 0, image.Width, image.Height), + sourceToTransformedCanvas); + + // The transform can produce fractional/max bounds; round up to whole pixels for target allocation. + Size targetSize = new( + Math.Max(1, (int)MathF.Ceiling(transformedDestinationRect.Width)), + Math.Max(1, (int)MathF.Ceiling(transformedDestinationRect.Height))); + + // ImageSharp.Transform expects output coordinates relative to the output bitmap origin (0,0). + // Shift transformed-canvas coordinates so transformedDestinationRect.Left/Top becomes 0,0. + Matrix3x2 sourceToTarget = sourceToTransformedCanvas + * Matrix3x2.CreateTranslation(-transformedDestinationRect.X, -transformedDestinationRect.Y); + + // Resample source pixels into the target bitmap using the computed source->target mapping. + return image.Clone(ctx => ctx.Transform( + image.Bounds, + sourceToTarget, + targetSize, + sampler ?? KnownResamplers.Bicubic)); + } + + /// + /// Maps a clipped source rectangle back to the corresponding destination rectangle. + /// + /// Original source rectangle. + /// Original destination rectangle. + /// Source rectangle clipped to image bounds. + /// The destination rectangle corresponding to the clipped source region. + private static RectangleF MapSourceClipToDestination( + Rectangle sourceRect, + RectangleF destinationRect, + Rectangle clippedSourceRect) + { + float scaleX = destinationRect.Width / sourceRect.Width; + float scaleY = destinationRect.Height / sourceRect.Height; + + float left = destinationRect.Left + ((clippedSourceRect.Left - sourceRect.Left) * scaleX); + float top = destinationRect.Top + ((clippedSourceRect.Top - sourceRect.Top) * scaleY); + float width = clippedSourceRect.Width * scaleX; + float height = clippedSourceRect.Height * scaleY; + + return new RectangleF(left, top, width, height); + } + + /// + /// Computes the axis-aligned bounding rectangle of a transformed rectangle. + /// + /// Input rectangle. + /// Transform matrix. + /// Axis-aligned bounds of the transformed rectangle. + private static RectangleF TransformRectangle(RectangleF rectangle, Matrix3x2 matrix) + { + Vector2 topLeft = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 topRight = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 bottomLeft = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + Vector2 bottomRight = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); + float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); + float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X)); + float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y)); + + return RectangleF.FromLTRB(left, top, right, bottom); + } + + /// + /// Disposes image resources retained for deferred draw execution. + /// + private void DisposePendingImageResources() + { + if (this.pendingImageResources.Count == 0) + { + return; + } + + // Release deferred image resources once queued operations have executed. + for (int i = 0; i < this.pendingImageResources.Count; i++) + { + this.pendingImageResources[i].Dispose(); + } + + this.pendingImageResources.Clear(); + } } diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index cc4fb6ff3..6198c2aa4 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -11,21 +11,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public class ImageBrush : Brush { - /// - /// The image to paint. - /// - private readonly Image image; - - /// - /// The region of the source image we will be using to paint. - /// - private readonly RectangleF region; - - /// - /// The offet to apply to the source image while applying the imagebrush - /// - private readonly Point offset; - /// /// Initializes a new instance of the class. /// @@ -73,24 +58,30 @@ public ImageBrush(Image image, RectangleF region) /// public ImageBrush(Image image, RectangleF region, Point offset) { - this.image = image; - this.region = RectangleF.Intersect(image.Bounds, region); - this.offset = offset; + this.SourceImage = image; + this.SourceRegion = RectangleF.Intersect(image.Bounds, region); + this.Offset = offset; } + internal Image SourceImage { get; } + + internal RectangleF SourceRegion { get; } + + internal Point Offset { get; } + /// public override bool Equals(Brush? other) { if (other is ImageBrush ib) { - return ib.image == this.image && ib.region == this.region; + return ib.SourceImage == this.SourceImage && ib.SourceRegion == this.SourceRegion; } return false; } /// - public override int GetHashCode() => HashCode.Combine(this.image, this.region); + public override int GetHashCode() => HashCode.Combine(this.SourceImage, this.SourceRegion); /// public override BrushApplicator CreateApplicator( @@ -99,13 +90,13 @@ public override BrushApplicator CreateApplicator( Buffer2DRegion targetRegion, RectangleF region) { - if (this.image is Image specificImage) + if (this.SourceImage is Image specificImage) { - return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.region, this.offset, false); + return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.SourceRegion, this.Offset, false); } - specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.region, this.offset, true); + specificImage = this.SourceImage.CloneAs(); + return new ImageBrushApplicator(configuration, options, targetRegion, specificImage, region, this.SourceRegion, this.Offset, true); } /// diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 7ebf0719d..3fc4441a0 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -26,6 +26,10 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel + => true; + public void FillPath( ICanvasFrame target, IPath path, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index dde036b6b..1de55e78e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -16,7 +16,8 @@ public class WebGPUDrawingBackendTests { [Theory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] - public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) + public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { DrawingOptions drawingOptions = new() { @@ -26,13 +27,23 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImagePro RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); - using Image defaultImage = provider.GetImage(); + using Image defaultImage = provider.GetImage(); defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + defaultImage.DebugSave( + provider, + "DefaultBackend_FillPath", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); - using Image webGpuImage = provider.GetImage(); + using Image webGpuImage = provider.GetImage(); using WebGPUDrawingBackend backend = new(); webGpuImage.Configuration.SetDrawingBackend(backend); webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + webGpuImage.DebugSave( + provider, + "WebGPUBackend_FillPath", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); Assert.True(backend.TestingPrepareCoverageCallCount > 0); Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); @@ -48,9 +59,81 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImagePro comparer.VerifySimilarity(defaultImage, webGpuImage); } + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + RectangularPolygon polygon = new(36.5F, 26.25F, 312.5F, 188.5F); + Brush clearBrush = Brushes.Solid(Color.White); + + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + + using Image defaultImage = new(384, 256); + using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + { + defaultCanvas.Fill(clearBrush, clearOptions); + defaultCanvas.FillPath(polygon, brush, drawingOptions); + defaultCanvas.Flush(); + } + + defaultImage.DebugSave( + provider, + "DefaultBackend_FillPath_ImageBrush", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = new(384, 256); + using WebGPUDrawingBackend backend = new(); + Configuration webGpuConfiguration = Configuration.Default.Clone(); + webGpuConfiguration.SetDrawingBackend(backend); + + using (DrawingCanvas webGpuCanvas = new(webGpuConfiguration, GetFrameRegion(webGpuImage))) + { + webGpuCanvas.Fill(clearBrush, clearOptions); + webGpuCanvas.FillPath(polygon, brush, drawingOptions); + webGpuCanvas.Flush(); + } + + webGpuImage.DebugSave( + provider, + "WebGPUBackend_FillPath_ImageBrush", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.True(backend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); + Assert.Equal(0, backend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(backend); + if (backend.TestingIsGPUReady) + { + Assert.True(backend.TestingGPUCompositeCoverageCallCount > 0); + } + + AssertGpuPathWhenRequired(backend); + + ImageComparer comparer = ImageComparer.TolerantPercentage(1F); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + [Theory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] - public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) + public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { DrawingOptions drawingOptions = new() { @@ -87,13 +170,23 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImagePro IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); - using Image defaultImage = provider.GetImage(); + using Image defaultImage = provider.GetImage(); defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); + defaultImage.DebugSave( + provider, + "DefaultBackend_FillPath_NonZeroNestedContours", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); - using Image webGpuImage = provider.GetImage(); + using Image webGpuImage = provider.GetImage(); using WebGPUDrawingBackend backend = new(); webGpuImage.Configuration.SetDrawingBackend(backend); webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); + webGpuImage.DebugSave( + provider, + "WebGPUBackend_FillPath_NonZeroNestedContours", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); Assert.True(backend.TestingPrepareCoverageCallCount > 0); Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); @@ -105,13 +198,28 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImagePro // but non-zero winding semantics must still match. Assert.Equal(defaultImage[128, 128], webGpuImage[128, 128]); + ImageComparer referenceComparer = ImageComparer.TolerantPercentage(0.5F); + defaultImage.CompareToReferenceOutput( + referenceComparer, + provider, + "FillPath_NonZeroNestedContours_Expected", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + webGpuImage.CompareToReferenceOutput( + referenceComparer, + provider, + "FillPath_NonZeroNestedContours_Expected", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); comparer.VerifySimilarity(defaultImage, webGpuImage); } [Theory] [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] - public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) + public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) + where TPixel : unmanaged, IPixel { Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); RichTextOptions textOptions = new(font) @@ -128,7 +236,7 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag Brush brush = Brushes.Solid(Color.Black); Pen pen = Pens.Solid(Color.OrangeRed, 2F); - using Image defaultImage = provider.GetImage(); + using Image defaultImage = provider.GetImage(); defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); defaultImage.DebugSave( provider, @@ -136,7 +244,7 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - using Image webGpuImage = provider.GetImage(); + using Image webGpuImage = provider.GetImage(); using WebGPUDrawingBackend backend = new(); webGpuImage.Configuration.SetDrawingBackend(backend); webGpuImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); @@ -160,7 +268,8 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag [Theory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] - public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput(TestImageProvider provider) + public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { DrawingOptions drawingOptions = new() { @@ -178,8 +287,8 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - using Image defaultImage = provider.GetImage(); - using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) { defaultCanvas.Fill(clearBrush, clearOptions); defaultCanvas.FillPath(polygon, brush, drawingOptions); @@ -194,7 +303,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu using WebGPUDrawingBackend backend = new(); Assert.True( - WebGPUTestNativeSurfaceAllocator.TryCreate( + WebGPUTestNativeSurfaceAllocator.TryCreate( backend, defaultImage.Width, defaultImage.Height, @@ -211,19 +320,19 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu Configuration configuration = Configuration.Default.Clone(); configuration.SetDrawingBackend(backend); - using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); + using DrawingCanvas canvas = + new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); canvas.Fill(clearBrush, clearOptions); canvas.FillPath(polygon, brush, drawingOptions); canvas.Flush(); Assert.True( - WebGPUTestNativeSurfaceAllocator.TryReadTexture( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( backend, textureHandle, defaultImage.Width, defaultImage.Height, - out Image webGpuImage, + out Image webGpuImage, out string readError), readError); @@ -247,7 +356,8 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu [Theory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] - public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput(TestImageProvider provider) + public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { DrawingOptions drawingOptions = new() { @@ -266,11 +376,11 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - using Image defaultImage = provider.GetImage(); - using DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage)); + using Image defaultImage = provider.GetImage(); + using DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage)); defaultCanvas.Fill(clearBrush, clearOptions); - using (DrawingCanvas defaultRegionCanvas = defaultCanvas.CreateRegion(region)) + using (DrawingCanvas defaultRegionCanvas = defaultCanvas.CreateRegion(region)) { defaultRegionCanvas.FillPath(localPolygon, brush, drawingOptions); } @@ -283,7 +393,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef using WebGPUDrawingBackend backend = new(); Assert.True( - WebGPUTestNativeSurfaceAllocator.TryCreate( + WebGPUTestNativeSurfaceAllocator.TryCreate( backend, defaultImage.Width, defaultImage.Height, @@ -300,10 +410,10 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef Configuration configuration = Configuration.Default.Clone(); configuration.SetDrawingBackend(backend); - using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); + using DrawingCanvas canvas = + new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); canvas.Fill(clearBrush, clearOptions); - using (DrawingCanvas regionCanvas = canvas.CreateRegion(region)) + using (DrawingCanvas regionCanvas = canvas.CreateRegion(region)) { regionCanvas.FillPath(localPolygon, brush, drawingOptions); } @@ -314,7 +424,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef textureHandle, defaultImage.Width, defaultImage.Height, - out Image webGpuImage, + out Image webGpuImage, out string readError), readError); @@ -326,11 +436,6 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - int defaultCoveragePixels = CountNonBackgroundPixels(defaultImage, Color.White); - int webGpuCoveragePixels = CountNonBackgroundPixels(webGpuImage, Color.White); - Assert.True(defaultCoveragePixels > 0, "Default backend produced no subregion fill coverage."); - Assert.True(webGpuCoveragePixels > 0, "WebGPU backend produced no subregion fill coverage."); - ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); comparer.VerifySimilarity(defaultImage, webGpuImage); } @@ -343,7 +448,8 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef [Theory] [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] - public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + where TPixel : unmanaged, IPixel { Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); RichTextOptions textOptions = new(font) @@ -360,7 +466,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider defaultImage = provider.GetImage(); + using Image defaultImage = provider.GetImage(); defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); defaultImage.DebugSave( provider, @@ -368,7 +474,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider webGpuImage = provider.GetImage(); + using Image webGpuImage = provider.GetImage(); using WebGPUDrawingBackend backend = new(); webGpuImage.Configuration.SetDrawingBackend(backend); @@ -380,13 +486,6 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider 0, "Default backend produced no text coverage."); - Assert.True( - webGpuCoveragePixels >= (defaultCoveragePixels * 9) / 10, - $"WebGPU text coverage is too low. default={defaultCoveragePixels}, webgpu={webGpuCoveragePixels}"); - ImageComparer comparer = ImageComparer.TolerantPercentage(2F); comparer.VerifySimilarity(defaultImage, webGpuImage); @@ -437,46 +536,22 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) backend.TestingFallbackCompositeCoverageCallCount); } - private static int CountNonBackgroundPixels(Image image, Color background) - { - Rgba32 bg = background.ToPixel(); - Buffer2D buffer = image.Frames.RootFrame.PixelBuffer; - int count = 0; - for (int y = 0; y < buffer.Height; y++) - { - Span row = buffer.DangerousGetRowSpan(y); - for (int x = 0; x < row.Length; x++) - { - Rgba32 pixel = row[x]; - if (Math.Abs(pixel.R - bg.R) > 2 || - Math.Abs(pixel.G - bg.G) > 2 || - Math.Abs(pixel.B - bg.B) > 2 || - Math.Abs(pixel.A - bg.A) > 2) - { - count++; - } - } - } - - return count; - } - - private static Buffer2DRegion GetFrameRegion(Image image) + private static Buffer2DRegion GetFrameRegion(Image image) + where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); private sealed class NativeSurfaceOnlyFrame : ICanvasFrame where TPixel : unmanaged, IPixel { - private readonly Rectangle bounds; private readonly NativeSurface surface; public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) { - this.bounds = bounds; + this.Bounds = bounds; this.surface = surface; } - public Rectangle Bounds => this.bounds; + public Rectangle Bounds { get; } public bool TryGetCpuRegion(out Buffer2DRegion region) { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 8cd75c8d9..1acdee747 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -36,8 +36,37 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() Assert.Same(brushB, backend.LastBatch.Commands[1].Brush); } + [Fact] + public void Flush_WhenAnyBrushUnsupported_DisablesSharedFlushId() + { + Configuration configuration = new(); + CapturingBackend backend = new() + { + IsBrushSupported = static brush => brush is SolidBrush + }; + + using Image image = new(40, 40); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + using DrawingCanvas canvas = new(configuration, backend, new CpuCanvasFrame(region)); + + IPath pathA = new RectangularPolygon(2, 2, 12, 12); + IPath pathB = new RectangularPolygon(18, 18, 12, 12); + DrawingOptions options = new(); + + canvas.FillPath(pathA, Brushes.Solid(Color.Red), options); + canvas.FillPath(pathB, Brushes.Horizontal(Color.Blue), options); + canvas.Flush(); + + Assert.NotEmpty(backend.Batches); + Assert.All(backend.Batches, static batch => Assert.Equal(0, batch.FlushId)); + } + private sealed class CapturingBackend : IDrawingBackend { + public Func IsBrushSupported { get; init; } = static _ => true; + + public List Batches { get; } = []; + public bool HasBatch { get; private set; } public CompositionBatch LastBatch { get; private set; } = new( @@ -62,6 +91,10 @@ public void FillPath( => batcher.AddComposition( CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel + => this.IsBrushSupported(brush); + public void FlushCompositions( Configuration configuration, ICanvasFrame target, @@ -70,6 +103,7 @@ public void FlushCompositions( { this.LastBatch = compositionBatch; this.HasBatch = true; + this.Batches.Add(compositionBatch); } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs new file mode 100644 index 000000000..6a3b6f09b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +[GroupOutput("Drawing")] +public class DrawingCanvasDrawImageTests +{ + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void DrawImage_WithRotationTransform_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(384, 256); + + using DrawingCanvas canvas = new( + provider.Configuration, + new Buffer2DRegion(target.Frames.RootFrame.PixelBuffer, target.Bounds)); + + GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) + }; + + canvas.Fill(Brushes.Solid(Color.White), clearOptions); + canvas.DrawImage( + foreground, + foreground.Bounds, + new RectangleF(72, 48, 240, 160), + options, + KnownResamplers.NearestNeighbor); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index dd82f4c28..e1909c278 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -109,6 +109,10 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { + public bool IsCompositionBrushSupported(Brush brush) + where TPixel : unmanaged, IPixel + => true; + public void FillPath( ICanvasFrame target, IPath path, From 1d94c731a6f98c25e87102b2c51874d4c152cf26 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Feb 2026 09:59:52 +1000 Subject: [PATCH 018/136] Per-brush instance payloads for WebGPU composite --- .../Brushes/IWebGPUBrushComposer.cs | 12 +- .../WebGPUCompositeCommonParameters.cs | 50 +++++++ .../WebGPUImageBrushComposer{TPixel}.cs | 58 +++++++- .../Brushes/WebGPUSolidBrushComposer.cs | 40 +++++- .../Shaders/CoverageRasterizationShader.cs | Bin 1416 -> 1484 bytes .../Shaders/ImageBrushCompositeShader.cs | Bin 4354 -> 4162 bytes .../Shaders/SolidBrushCompositeShader.cs | Bin 3463 -> 3043 bytes .../WebGPUCompositeInstanceData.cs | 30 ---- .../WebGPUDrawingBackend.cs | 134 +++++++++++------- .../WebGPUFlushContext.cs | 21 +-- 10 files changed, 232 insertions(+), 113 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUCompositeInstanceData.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs index 51c09f561..dcd17a792 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs @@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; /// internal unsafe interface IWebGPUBrushComposer { + /// + /// Gets the size in bytes of this composer's instance payload. + /// + public nuint InstanceDataSizeInBytes { get; } + /// /// Gets or creates the render pipeline required by this brush composer. /// @@ -23,10 +28,11 @@ public bool TryGetOrCreatePipeline( out string? error); /// - /// Populates brush-specific fields in the shared composite instance payload. + /// Writes one brush-specific instance payload into . /// - /// The instance payload to update. - public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance); + /// The command values shared by every brush payload. + /// The destination bytes for the payload. + public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination); /// /// Creates the bind group for this brush using the current coverage and instance buffers. diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs new file mode 100644 index 000000000..5726aa5a7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// Common per-command composition values shared by all brush composers. +/// +internal readonly struct WebGPUCompositeCommonParameters +{ + public readonly int SourceOffsetX; + + public readonly int SourceOffsetY; + + public readonly int DestinationX; + + public readonly int DestinationY; + + public readonly int DestinationWidth; + + public readonly int DestinationHeight; + + public readonly int TargetWidth; + + public readonly int TargetHeight; + + public readonly float BlendPercentage; + + public WebGPUCompositeCommonParameters( + int sourceOffsetX, + int sourceOffsetY, + int destinationX, + int destinationY, + int destinationWidth, + int destinationHeight, + int targetWidth, + int targetHeight, + float blendPercentage) + { + this.SourceOffsetX = sourceOffsetX; + this.SourceOffsetY = sourceOffsetY; + this.DestinationX = destinationX; + this.DestinationY = destinationY; + this.DestinationWidth = destinationWidth; + this.DestinationHeight = destinationHeight; + this.TargetWidth = targetWidth; + this.TargetHeight = targetHeight; + this.BlendPercentage = blendPercentage; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs index b602ecc33..1c2484fe3 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Silk.NET.WebGPU; using SixLabors.ImageSharp.PixelFormats; @@ -32,6 +35,9 @@ private WebGPUImageBrushComposer( this.imageBrushOriginY = imageBrushOriginY; } + /// + public nuint InstanceDataSizeInBytes => (nuint)Unsafe.SizeOf(); + /// public bool TryGetOrCreatePipeline( WebGPUFlushContext flushContext, @@ -72,14 +78,30 @@ public static WebGPUImageBrushComposer Create( } /// - public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance) + public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination) { - instance.ImageRegionX = this.sourceRegion.X; - instance.ImageRegionY = this.sourceRegion.Y; - instance.ImageRegionWidth = this.sourceRegion.Width; - instance.ImageRegionHeight = this.sourceRegion.Height; - instance.ImageBrushOriginX = this.imageBrushOriginX; - instance.ImageBrushOriginY = this.imageBrushOriginY; + ImageBrushInstanceData data = new() + { + SourceOffsetX = common.SourceOffsetX, + SourceOffsetY = common.SourceOffsetY, + DestinationX = common.DestinationX, + DestinationY = common.DestinationY, + DestinationWidth = common.DestinationWidth, + DestinationHeight = common.DestinationHeight, + TargetWidth = common.TargetWidth, + TargetHeight = common.TargetHeight, + ImageRegionX = this.sourceRegion.X, + ImageRegionY = this.sourceRegion.Y, + ImageRegionWidth = this.sourceRegion.Width, + ImageRegionHeight = this.sourceRegion.Height, + ImageBrushOriginX = this.imageBrushOriginX, + ImageBrushOriginY = this.imageBrushOriginY, + Padding0 = 0, + Padding1 = 0, + BlendData = new Vector4(common.BlendPercentage, 0, 0, 0) + }; + + MemoryMarshal.Write(destination, in data); } /// @@ -181,4 +203,26 @@ private static bool TryCreateBindGroupLayout( error = null; return true; } + + [StructLayout(LayoutKind.Sequential)] + private struct ImageBrushInstanceData + { + public int SourceOffsetX; + public int SourceOffsetY; + public int DestinationX; + public int DestinationY; + public int DestinationWidth; + public int DestinationHeight; + public int TargetWidth; + public int TargetHeight; + public int ImageRegionX; + public int ImageRegionY; + public int ImageRegionWidth; + public int ImageRegionHeight; + public int ImageBrushOriginX; + public int ImageBrushOriginY; + public int Padding0; + public int Padding1; + public Vector4 BlendData; + } } diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs index bb1a651a3..9cccf2f01 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Silk.NET.WebGPU; namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; @@ -21,6 +23,9 @@ public WebGPUSolidBrushComposer(SolidBrush brush) this.color = brush.Color.ToScaledVector4(); } + /// + public nuint InstanceDataSizeInBytes => (nuint)Unsafe.SizeOf(); + /// public bool TryGetOrCreatePipeline( WebGPUFlushContext flushContext, @@ -36,8 +41,24 @@ public bool TryGetOrCreatePipeline( out error); /// - public void PopulateInstanceData(ref WebGPUCompositeInstanceData instance) - => instance.SolidBrushColor = this.color; + public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination) + { + SolidBrushInstanceData data = new() + { + SourceOffsetX = common.SourceOffsetX, + SourceOffsetY = common.SourceOffsetY, + DestinationX = common.DestinationX, + DestinationY = common.DestinationY, + DestinationWidth = common.DestinationWidth, + DestinationHeight = common.DestinationHeight, + TargetWidth = common.TargetWidth, + TargetHeight = common.TargetHeight, + BlendData = new Vector4(common.BlendPercentage, 0, 0, 0), + SolidBrushColor = this.color + }; + + MemoryMarshal.Write(destination, in data); + } /// public BindGroup* CreateBindGroup( @@ -122,4 +143,19 @@ private static bool TryCreateBindGroupLayout( error = null; return true; } + + [StructLayout(LayoutKind.Sequential)] + private struct SolidBrushInstanceData + { + public int SourceOffsetX; + public int SourceOffsetY; + public int DestinationX; + public int DestinationY; + public int DestinationWidth; + public int DestinationHeight; + public int TargetWidth; + public int TargetHeight; + public Vector4 BlendData; + public Vector4 SolidBrushColor; + } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs index 4377879fab684e3a598b70d29e2beb04561af73c..5ee56afeab3fae5fa2989bc7a00a3f05f2b554f9 100644 GIT binary patch delta 200 zcmXwxI|>3p5JU|WWDQLXiVegMenP=W#K1r>@c_NEtq#u4EZVCWh^e9ANj!t8XA&F1 zf(l+0yj$&EyT7i30j43#t>d29jA4R52zfz5X2&U#GnB;|EN4hb2rVD5Wy(R$1*2E+ z?S2G}y5`=vBIN@J&I<3zVbvXK;0x!VOJ+Su*ki4gZG)j`kBrkMFEGVqIY%ow3wPT6 a$D-1!{>&gg#c#TWpEz#_5MT$AG%b@>nmT65KM0RT0u BBAoyL diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs index aeb09d7d4483f6ec5124ec5c68aff482fe9fafbd..43f8ab1022cdb75e22c43809144bebbce779f445 100644 GIT binary patch delta 287 zcmZotI;1e+V7>Zb@ctszP#pUU5lcUWr0dVsdtBib7JQ0+3&lnXC}t zs*sXclBl4Om#>hKnpmKan3I#A43x^x)70Zq00N*{o_WbRr75Y!3W*9inI)-3i8%_P z+6F+NkXM?MqW~1i&CE+I$uCk!1LBelnN$TJH?u5Np*+2yRKZ!nF~CznBdM|^RZCNE z@^JxOX|Ty4)0LEz;Oyd(qSEA&i3>zFD>BM6P1a!MnOx4iiqQ%v!r}sSPlYjH_Y`(;MhDn$KZZOc2QVVOY GS}p)Hv0E_! delta 457 zcmX@4(4;ispmjk}W?5oMszPx|Vo7GQLQ!gBN`78Wr9x6=NosVgf^&XKsuPe|tYFKf z00hxs62#Ken>>+GMBF()w;;bbvn16s4`@VQa;i&WNuoman{P7yW7LFe0BI`D z&&f=QPbw-c&WKOW&&e;cQYcGJHnB-FHnN+1m3fC;v6Vt%QBh*04OVj}d$7n)KEkqP zvM(#2rlF>SLuOt{YEeOc4$uc0X+Tm_ArtD1_{_YN)CwzwQe&gZd8}!SI+F!h#UxN= z5vnJjWOd*wj)qB3e#kDmS)47NSuMY`q@c7!4<-RIRlyd`n#mcQrv%VlyxE28Et3Gq zP-GK;MoUhv;S!nL!^<;y8kgqg9^S=Fh(J>Zb@ctszP#pUU5lcUWr0dVsdtBib7JQ0+3&lnXC}t zs*sXclBl4Om#>hKnpmKan3I#A43x^x)70Zq00N*{o_WbRr75Y!3W*9inI)-3i8%_P z+6F+NkXM?MqW~1i&CE+I$uCk!1LBelnN$TJH?u5Np*+2yRKZ!nF~CznBdM|^RZCNE zvJbDWG}vU2=}JmUaCUJ?QE76?#04Uo6&ZyYCu=bCOfF}ZpX|x3HJOu@ck)YSp~+jB za~Q3FDp*{Aj-Kqm(gBo-nLMA>bn{czT;|DboD(>+GMBF()w;;bbvn16s4`@VQa;i&WNuomalW#J9LZ}Dn$;?em zPmM21P0!5Fi?6U!$TT+6fg6aSppt-s$@7>bIpIc5u4a<0hwze$N{ch%^NTXmGx1r2 zq70j5@db$~23Q;c5;ug3qd0%^FDA9gmzk6&D=>G<7h5SL78NB{+F%VLyU9nGJIq0T zcF4(328Mlpo`#{Of( } IWebGPUBrushComposer[] composers = new IWebGPUBrushComposer[commandCount]; - for (int i = 0; i < commandCount; i++) + for (int i = 0; i < composers.Length; i++) { composers[i] = WebGPUBrushComposerFactory.Create(flushContext, commands[i]); } - nuint instanceBytes = checked((nuint)commandCount * (nuint)Unsafe.SizeOf()); + nuint totalInstanceBytes = 0; + nuint maxInstanceBytes = 0; + for (int i = 0; i < composers.Length; i++) + { + nuint instanceBytes = composers[i].InstanceDataSizeInBytes; + if (instanceBytes == 0) + { + error = "Brush composer returned an empty instance payload."; + return false; + } + + totalInstanceBytes = checked(totalInstanceBytes + AlignToStorageBufferOffset(instanceBytes)); + if (instanceBytes > maxInstanceBytes) + { + maxInstanceBytes = instanceBytes; + } + } + nuint instanceOffset = flushContext.InstanceBufferWriteOffset; - nuint requiredCapacity = checked(instanceOffset + instanceBytes); + nuint requiredCapacity = checked(instanceOffset + totalInstanceBytes); // If the buffer exists but cannot fit at the current offset, flush pending // draws and reset so the next batch starts at offset 0. @@ -369,7 +386,7 @@ private bool TryCompositeBatch( } instanceOffset = 0; - requiredCapacity = instanceBytes; + requiredCapacity = totalInstanceBytes; } if (!flushContext.EnsureInstanceBufferCapacity(requiredCapacity, Math.Max(requiredCapacity, CompositeInstanceBufferSize)) || @@ -380,62 +397,71 @@ private bool TryCompositeBatch( return false; } - Span instances = flushContext.GetCompositeInstanceSpan(commandCount); - int targetWidth = flushContext.TargetBounds.Width; - int targetHeight = flushContext.TargetBounds.Height; - for (int i = 0; i < commandCount; i++) + byte[]? rentedInstanceData = null; + try { - PreparedCompositionCommand command = commands[i]; - int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; - int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; - - instances[i] = new WebGPUCompositeInstanceData + rentedInstanceData = ArrayPool.Shared.Rent(checked((int)maxInstanceBytes)); + Span instanceScratch = rentedInstanceData; + nuint commandOffset = instanceOffset; + int targetWidth = flushContext.TargetBounds.Width; + int targetHeight = flushContext.TargetBounds.Height; + for (int i = 0; i < composers.Length; i++) { - SourceOffsetX = command.SourceOffset.X, - SourceOffsetY = command.SourceOffset.Y, - DestinationX = destinationX, - DestinationY = destinationY, - DestinationWidth = command.DestinationRegion.Width, - DestinationHeight = command.DestinationRegion.Height, - TargetWidth = targetWidth, - TargetHeight = targetHeight, - BlendData = new Vector4(command.GraphicsOptions.BlendPercentage, 0, 0, 0) - }; + IWebGPUBrushComposer composer = composers[i]; + PreparedCompositionCommand command = commands[i]; + nuint instanceBytes = composer.InstanceDataSizeInBytes; + int instanceBytesInt = checked((int)instanceBytes); + int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; + int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; + WebGPUCompositeCommonParameters common = new( + command.SourceOffset.X, + command.SourceOffset.Y, + destinationX, + destinationY, + command.DestinationRegion.Width, + command.DestinationRegion.Height, + targetWidth, + targetHeight, + command.GraphicsOptions.BlendPercentage); + + Span payload = instanceScratch[..instanceBytesInt]; + composer.WriteInstanceData(in common, payload); + + fixed (byte* payloadPtr = payload) + { + // QueueWriteBuffer copies source bytes into driver-owned staging immediately. + flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, commandOffset, payloadPtr, instanceBytes); + } - composers[i].PopulateInstanceData(ref instances[i]); - } + if (!composer.TryGetOrCreatePipeline(flushContext, out RenderPipeline* pipeline, out string? pipelineError)) + { + error = pipelineError ?? "Failed to create composite pipeline."; + return false; + } - fixed (WebGPUCompositeInstanceData* instancesPtr = instances) - { - // QueueWriteBuffer copies source bytes into driver-owned staging immediately. - flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, instanceOffset, instancesPtr, instanceBytes); - } + BindGroup* bindGroup = composer.CreateBindGroup( + flushContext, + coverageEntry.GPUCoverageView, + commandOffset, + instanceBytes); + + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); + flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, 0); + commandOffset = checked(commandOffset + AlignToStorageBufferOffset(instanceBytes)); + } - for (int i = 0; i < commandCount; i++) + flushContext.AdvanceInstanceBufferOffset(commandOffset); + return true; + } + finally { - IWebGPUBrushComposer composer = composers[i]; - - if (!composer.TryGetOrCreatePipeline(flushContext, out RenderPipeline* pipeline, out string? pipelineError)) + if (rentedInstanceData is not null) { - error = pipelineError ?? "Failed to create composite pipeline."; - return false; + ArrayPool.Shared.Return(rentedInstanceData); } - - BindGroup* bindGroup = composer.CreateBindGroup( - flushContext, - coverageEntry.GPUCoverageView, - instanceOffset, - instanceBytes); - - flushContext.TrackBindGroup(bindGroup); - flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); - flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); - flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, (uint)i); } - - flushContext.AdvanceInstanceBufferOffset(instanceOffset + instanceBytes); - - return true; } private bool TryFinalizeFlush( @@ -649,6 +675,10 @@ public void Dispose() private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nuint AlignToStorageBufferOffset(nuint value) + => (value + 255) & ~(nuint)255; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) where T : struct diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 15aa88508..168ac909b 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -29,7 +29,6 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable private bool ownsTargetTexture; private bool ownsTargetView; private bool ownsReadbackBuffer; - private WebGPUCompositeInstanceData[]? compositeInstanceData; private readonly List transientBindGroups = []; private readonly List transientTextureViews = []; private readonly List transientTextures = []; @@ -92,23 +91,6 @@ private WebGPUFlushContext( public RenderPassEncoder* PassEncoder { get; private set; } - public Span GetCompositeInstanceSpan(int count) - { - if (count <= 0) - { - return []; - } - - WebGPUCompositeInstanceData[]? cached = this.compositeInstanceData; - if (cached is null || cached.Length < count) - { - cached = new WebGPUCompositeInstanceData[count]; - this.compositeInstanceData = cached; - } - - return cached.AsSpan(0, count); - } - public static WebGPUFlushContext Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, @@ -540,7 +522,6 @@ public void Dispose() this.ownsReadbackBuffer = false; this.ownsTargetView = false; this.ownsTargetTexture = false; - this.compositeInstanceData = null; this.RuntimeLease.Dispose(); this.disposed = true; @@ -1189,6 +1170,8 @@ private bool TryCreateCompositeInfrastructure( return false; } + // The native wgpu C API expects a null-terminated byte* for Code. + // Shader spans include the \0 terminator so fixed pinning is sufficient. fixed (byte* shaderCodePtr = shaderCode) { ShaderModuleWGSLDescriptor wgslDescriptor = new() From edc9b8cb203ec6599ceef664ff13c3f59a565410 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Feb 2026 10:24:11 +1000 Subject: [PATCH 019/136] Move WGPUTextureFormat note to code comment --- src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs index 278f4690e..33882361f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs @@ -5,10 +5,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Public WebGPU texture format identifiers used by . -/// Numeric values intentionally match WGPUTextureFormat. /// public enum WebGPUTextureFormatId { + // Numeric values intentionally match WGPUTextureFormat. + /// /// Single-channel 8-bit normalized unsigned format. /// From 7e7ce5d9fbd790041c16206726ce585ec893a529 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 24 Feb 2026 15:41:43 +1000 Subject: [PATCH 020/136] Switch compositing to compute shaders --- .../Brushes/IWebGPUBrushComposer.cs | 13 +- .../Brushes/WebGPUBrushComposerCacheKey.cs | 44 ++ .../Brushes/WebGPUBrushComposerFactory.cs | 11 +- .../WebGPUCompositeCommonParameters.cs | 30 +- .../WebGPUImageBrushComposer{TPixel}.cs | 107 ++- .../Brushes/WebGPUSolidBrushComposer.cs | 96 ++- .../Shaders/CompositeDestinationBlitShader.cs | Bin 0 -> 1828 bytes .../Shaders/CompositeDestinationInitShader.cs | Bin 0 -> 1036 bytes .../ImageBrushCompositeComputeShader.cs | 284 ++++++++ .../Shaders/ImageBrushCompositeShader.cs | Bin 4162 -> 0 bytes .../SolidBrushCompositeComputeShader.cs | 260 +++++++ .../Shaders/SolidBrushCompositeShader.cs | Bin 3043 -> 0 bytes .../WebGPUDrawingBackend.cs | 665 ++++++++++++++++-- .../WebGPUFlushContext.cs | 294 +++++++- .../WebGPUNativeSurfaceFactory.cs | 9 +- .../WebGPUSurfaceCapability.cs | 12 +- .../WebGPUTestNativeSurfaceAllocator.cs | 5 +- .../Backends/WebGPUDrawingBackendTests.cs | 113 +++ 18 files changed, 1784 insertions(+), 159 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs index dcd17a792..7907810a9 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using Silk.NET.WebGPU; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; @@ -16,15 +17,15 @@ internal unsafe interface IWebGPUBrushComposer public nuint InstanceDataSizeInBytes { get; } /// - /// Gets or creates the render pipeline required by this brush composer. + /// Gets or creates the compute pipeline required by this brush composer. /// /// The active WebGPU flush context. - /// The created or cached render pipeline. + /// The created or cached compute pipeline. /// The error message when pipeline acquisition fails. /// if the pipeline is available; otherwise . public bool TryGetOrCreatePipeline( WebGPUFlushContext flushContext, - out RenderPipeline* pipeline, + out ComputePipeline* pipeline, out string? error); /// @@ -35,16 +36,20 @@ public bool TryGetOrCreatePipeline( public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination); /// - /// Creates the bind group for this brush using the current coverage and instance buffers. + /// Creates the bind group for this brush using the current coverage and destination buffers. /// /// The active WebGPU flush context. /// The coverage texture view for the current batch. + /// The storage buffer containing destination pixels. + /// The byte size of . /// The instance buffer offset. /// The bound instance byte length. /// The created bind group. public BindGroup* CreateBindGroup( WebGPUFlushContext flushContext, TextureView* coverageView, + WgpuBuffer* destinationPixelsBuffer, + nuint destinationPixelsByteSize, nuint instanceOffset, nuint instanceBytes); } diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs new file mode 100644 index 000000000..5c615a74a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; + +/// +/// Batch-local brush composer cache key. +/// +internal readonly struct WebGPUBrushComposerCacheKey : IEquatable +{ + private readonly Brush brush; + private readonly Rectangle brushBounds; + private readonly bool includeBrushBounds; + + public WebGPUBrushComposerCacheKey(Brush brush, in Rectangle brushBounds, bool includeBrushBounds) + { + this.brush = brush; + this.brushBounds = brushBounds; + this.includeBrushBounds = includeBrushBounds; + } + + public bool Equals(WebGPUBrushComposerCacheKey other) + { + if (!ReferenceEquals(this.brush, other.brush) || + this.includeBrushBounds != other.includeBrushBounds) + { + return false; + } + + return !this.includeBrushBounds || this.brushBounds.Equals(other.brushBounds); + } + + public override bool Equals(object? obj) => obj is WebGPUBrushComposerCacheKey other && this.Equals(other); + + public override int GetHashCode() + { + int brushHash = RuntimeHelpers.GetHashCode(this.brush); + return this.includeBrushBounds + ? HashCode.Combine(brushHash, this.brushBounds, this.includeBrushBounds) + : HashCode.Combine(brushHash, this.includeBrushBounds); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs index aedc93b35..aa05904e7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs @@ -32,8 +32,6 @@ public static IWebGPUBrushComposer Create( in PreparedCompositionCommand command) where TPixel : unmanaged, IPixel { - Guard.NotNull(command.Brush, nameof(command.Brush)); - if (command.Brush is SolidBrush solidBrush) { return new WebGPUSolidBrushComposer(solidBrush); @@ -46,4 +44,13 @@ public static IWebGPUBrushComposer Create( throw new InvalidOperationException($"Unexpected brush type '{command.Brush.GetType().FullName}'."); } + + /// + /// Creates a cache key for reusing brush composers within one batch. + /// + public static WebGPUBrushComposerCacheKey CreateCacheKey(in PreparedCompositionCommand command) + { + bool includeBrushBounds = command.Brush is ImageBrush; + return new WebGPUBrushComposerCacheKey(command.Brush, command.BrushBounds, includeBrushBounds); + } } diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs index 5726aa5a7..7d4013181 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs @@ -20,12 +20,20 @@ internal readonly struct WebGPUCompositeCommonParameters public readonly int DestinationHeight; - public readonly int TargetWidth; + public readonly int DestinationBufferWidth; - public readonly int TargetHeight; + public readonly int DestinationBufferHeight; + + public readonly int DestinationBufferOriginX; + + public readonly int DestinationBufferOriginY; public readonly float BlendPercentage; + public readonly int ColorBlendingMode; + + public readonly int AlphaCompositionMode; + public WebGPUCompositeCommonParameters( int sourceOffsetX, int sourceOffsetY, @@ -33,9 +41,13 @@ public WebGPUCompositeCommonParameters( int destinationY, int destinationWidth, int destinationHeight, - int targetWidth, - int targetHeight, - float blendPercentage) + int destinationBufferWidth, + int destinationBufferHeight, + int destinationBufferOriginX, + int destinationBufferOriginY, + float blendPercentage, + int colorBlendingMode, + int alphaCompositionMode) { this.SourceOffsetX = sourceOffsetX; this.SourceOffsetY = sourceOffsetY; @@ -43,8 +55,12 @@ public WebGPUCompositeCommonParameters( this.DestinationY = destinationY; this.DestinationWidth = destinationWidth; this.DestinationHeight = destinationHeight; - this.TargetWidth = targetWidth; - this.TargetHeight = targetHeight; + this.DestinationBufferWidth = destinationBufferWidth; + this.DestinationBufferHeight = destinationBufferHeight; + this.DestinationBufferOriginX = destinationBufferOriginX; + this.DestinationBufferOriginY = destinationBufferOriginY; this.BlendPercentage = blendPercentage; + this.ColorBlendingMode = colorBlendingMode; + this.AlphaCompositionMode = alphaCompositionMode; } } diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs index 1c2484fe3..57ebefda8 100644 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs +++ b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs @@ -1,11 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; @@ -21,7 +21,12 @@ internal sealed unsafe class WebGPUImageBrushComposer : IWebGPUBrushComp private readonly Rectangle sourceRegion; private readonly int imageBrushOriginX; private readonly int imageBrushOriginY; + private BindGroup* cachedBindGroup; + private nint cachedCoverageView; + private nint cachedDestinationBuffer; + private nuint cachedInstanceBytes; private BindGroupLayout* bindGroupLayout; + private ComputePipeline* computePipeline; private WebGPUImageBrushComposer( TextureView* sourceTextureView, @@ -41,17 +46,32 @@ private WebGPUImageBrushComposer( /// public bool TryGetOrCreatePipeline( WebGPUFlushContext flushContext, - out RenderPipeline* pipeline, + out ComputePipeline* pipeline, out string? error) - => flushContext.DeviceState.TryGetOrCreateCompositePipeline( + { + if (this.computePipeline is not null) + { + pipeline = this.computePipeline; + error = null; + return true; + } + + bool success = flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( PipelineKey, - ImageBrushCompositeShader.Code, + ImageBrushCompositeComputeShader.Code, TryCreateBindGroupLayout, - flushContext.TextureFormat, out this.bindGroupLayout, out pipeline, out error); + if (success) + { + this.computePipeline = pipeline; + } + + return success; + } + /// /// Creates a composer for one image brush command. /// @@ -88,17 +108,20 @@ public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span public bool TryGetOrCreatePipeline( WebGPUFlushContext flushContext, - out RenderPipeline* pipeline, + out ComputePipeline* pipeline, out string? error) - => flushContext.DeviceState.TryGetOrCreateCompositePipeline( + { + if (this.computePipeline is not null) + { + pipeline = this.computePipeline; + error = null; + return true; + } + + bool success = flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( PipelineKey, - SolidBrushCompositeShader.Code, + SolidBrushCompositeComputeShader.Code, TryCreateBindGroupLayout, - flushContext.TextureFormat, out this.bindGroupLayout, out pipeline, out error); + if (success) + { + this.computePipeline = pipeline; + } + + return success; + } + /// public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination) { @@ -51,9 +72,12 @@ public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Spanu z9*`3s&OP^>YtLrzEzL^7_OSvxbYRO0xFq-)J)aL+=@C5<5G5fz|f+C#uLOPFdf zu4Qp@pfV$&0o5!&4rEWaF%g-2C&)A7dv_z!kV>hSw

cc_gnbi*ZE-CkaSJ6bm6t zh?J=Akfo9-dPk*V9CuQFi&c>k1zCSAzz2nD982g_u>*xLlq19m zjW#Oqwcd*~&+NGa>wxhH3Pz$MkEg`>JI573#4S8s0t4G33=i0)!Hi0jm z-PUjNLD|)|J-u}26VPv`YvCLQ?}Jrr8vG{PJ7MoETER?FrARrLzj>JWS%B4*S_gShg^cHT=2SRw{_~BjU5jDfpXPqz^T}cr=k(#Km+%i zzj{+=hGX4aGy1%>%12Y2Pz42chPQ%!#^~bUP0dMhN4G%NfwohVl&_MMuaJ~O61D@o zfECQ)ad_O!a%`qly4L^ODf!s?~cv}cYXwrH2|@l1~c epP(3aQx`qIza9RtS74M*pilm33Z0jlPK^!KqkIxRc{KWldSf*ASHn?U$n)I_-Hih*2|qWWf9&*u*{?u z)FG|A(*v{MOldi$vPoqM9xGp3Oz%aqeYjsx3JtM&{zLjSASKQlOOP>G@|YeiWwZcs zO(`v8h00BVS#&M`K@F^Qn`+F?<{Yy*$ZNd5fXd>+4lGuDgT!U*(2kkwVu)^646VQ_>iu0b)fCdx|x@KMO|ip(j`k zVYgHG*}{HZ`&}C*!a;A1Y`^vK%_+rspGb-`q)*RP6^&uIQ%BM5^}Ak}r*|El{@?O( H7My~=P*^n% literal 0 HcmV?d00001 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs new file mode 100644 index 000000000..d68d73943 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs @@ -0,0 +1,284 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class ImageBrushCompositeComputeShader +{ + // Compile-time constant backed by static PE data (no heap allocation). + public static ReadOnlySpan Code => + """ + struct ImageBrushCompositeData { + source_offset_x: i32, + source_offset_y: i32, + destination_x: i32, + destination_y: i32, + destination_width: i32, + destination_height: i32, + destination_buffer_width: i32, + destination_buffer_height: i32, + blend_percentage: f32, + color_blending_mode: i32, + alpha_composition_mode: i32, + _common_pad0: i32, + image_region_x: i32, + image_region_y: i32, + image_region_width: i32, + image_region_height: i32, + image_brush_origin_x: i32, + image_brush_origin_y: i32, + _pad0: i32, + _pad1: i32, + }; + + @group(0) @binding(0) + var coverage: texture_2d; + + @group(0) @binding(1) + var instance: ImageBrushCompositeData; + + @group(0) @binding(2) + var source_image: texture_2d; + + @group(0) @binding(3) + var destination_pixels: array>; + + fn overlay_value(backdrop: f32, source: f32) -> f32 { + if (backdrop <= 0.5) { + return 2.0 * backdrop * source; + } + + return 1.0 - (2.0 * (1.0 - source) * (1.0 - backdrop)); + } + + fn blend_color(backdrop: vec3, source: vec3, color_mode: i32) -> vec3 { + switch color_mode { + case 0 { + return source; + } + + case 1 { + return backdrop * source; + } + + case 2 { + return min(vec3(1.0), backdrop + source); + } + + case 3 { + return max(vec3(0.0), backdrop - source); + } + + case 4 { + return vec3(1.0) - ((vec3(1.0) - backdrop) * (vec3(1.0) - source)); + } + + case 5 { + return min(backdrop, source); + } + + case 6 { + return max(backdrop, source); + } + + case 7 { + return vec3( + overlay_value(backdrop.r, source.r), + overlay_value(backdrop.g, source.g), + overlay_value(backdrop.b, source.b)); + } + + case 8 { + return vec3( + overlay_value(source.r, backdrop.r), + overlay_value(source.g, backdrop.g), + overlay_value(source.b, backdrop.b)); + } + + default { + return source; + } + } + } + + fn unpremultiply(premultiplied_rgb: vec3, alpha: f32) -> vec4 { + let clamped_alpha = clamp(alpha, 0.0, 1.0); + if (clamped_alpha <= 0.0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + let color = clamp( + premultiplied_rgb / clamped_alpha, + vec3(0.0), + vec3(1.0)); + return vec4(color, clamped_alpha); + } + + fn compose_over(destination: vec4, source: vec4, blend: vec3) -> vec4 { + let source_weight = source.a; + let destination_weight = destination.a; + let blend_weight = source_weight * destination_weight; + let destination_only_weight = destination_weight - blend_weight; + let source_only_weight = source_weight - blend_weight; + let alpha = destination_only_weight + source_weight; + let premultiplied_color = + (destination.rgb * destination_only_weight) + + (source.rgb * source_only_weight) + + (blend * blend_weight); + return unpremultiply(premultiplied_color, alpha); + } + + fn compose_atop(destination: vec4, source: vec4, blend: vec3) -> vec4 { + let source_weight = source.a; + let destination_weight = destination.a; + let blend_weight = source_weight * destination_weight; + let destination_only_weight = destination_weight - blend_weight; + let premultiplied_color = + (destination.rgb * destination_only_weight) + + (blend * blend_weight); + return unpremultiply(premultiplied_color, destination_weight); + } + + fn compose_in(destination: vec4, source: vec4) -> vec4 { + let alpha = destination.a * source.a; + return unpremultiply(source.rgb * alpha, alpha); + } + + fn compose_out(destination: vec4, source: vec4) -> vec4 { + let alpha = (1.0 - destination.a) * source.a; + return unpremultiply(source.rgb * alpha, alpha); + } + + fn compose_xor(destination: vec4, source: vec4) -> vec4 { + let source_weight = 1.0 - destination.a; + let destination_weight = 1.0 - source.a; + let alpha = (source.a * source_weight) + (destination.a * destination_weight); + let premultiplied_color = + (source.a * source.rgb * source_weight) + + (destination.a * destination.rgb * destination_weight); + return unpremultiply(premultiplied_color, alpha); + } + + fn compose_pixel( + destination: vec4, + source: vec4, + blend_percentage: f32, + color_mode: i32, + alpha_mode: i32) -> vec4 { + let source_alpha = clamp(source.a * blend_percentage, 0.0, 1.0); + let source_color = clamp(source.rgb, vec3(0.0), vec3(1.0)); + let source_with_opacity = vec4(source_color, source_alpha); + let destination_color = clamp(destination.rgb, vec3(0.0), vec3(1.0)); + let destination_alpha = clamp(destination.a, 0.0, 1.0); + let destination_pixel = vec4(destination_color, destination_alpha); + + switch alpha_mode { + case 0 { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_over(destination_pixel, source_with_opacity, blend); + } + + case 1 { + return source_with_opacity; + } + + case 2 { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_atop(destination_pixel, source_with_opacity, blend); + } + + case 3 { + return compose_in(destination_pixel, source_with_opacity); + } + + case 4 { + return compose_out(destination_pixel, source_with_opacity); + } + + case 5 { + return destination_pixel; + } + + case 6 { + let blend = blend_color(source_color, destination_color, color_mode); + return compose_atop(source_with_opacity, destination_pixel, blend); + } + + case 7 { + let blend = blend_color(source_color, destination_color, color_mode); + return compose_over(source_with_opacity, destination_pixel, blend); + } + + case 8 { + return compose_in(source_with_opacity, destination_pixel); + } + + case 9 { + return compose_out(source_with_opacity, destination_pixel); + } + + case 10 { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + case 11 { + return compose_xor(destination_pixel, source_with_opacity); + } + + default { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_over(destination_pixel, source_with_opacity, blend); + } + } + } + + fn positive_mod(value: i32, divisor: i32) -> i32 { + return ((value % divisor) + divisor) % divisor; + } + + fn sample_brush(params: ImageBrushCompositeData, destination_x: i32, destination_y: i32) -> vec4 { + if (params.image_region_width <= 0 || params.image_region_height <= 0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + let source_x = positive_mod(destination_x - params.image_brush_origin_x, params.image_region_width) + params.image_region_x; + let source_y = positive_mod(destination_y - params.image_brush_origin_y, params.image_region_height) + params.image_region_y; + return textureLoad(source_image, vec2(source_x, source_y), 0); + } + + @compute @workgroup_size(8, 8, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + let params = instance; + let local_x = i32(global_id.x); + let local_y = i32(global_id.y); + if (local_x >= params.destination_width || local_y >= params.destination_height) { + return; + } + + let destination_pixel_x = params.destination_x + local_x; + let destination_pixel_y = params.destination_y + local_y; + if (destination_pixel_x < 0 || + destination_pixel_y < 0 || + destination_pixel_x >= params.destination_buffer_width || + destination_pixel_y >= params.destination_buffer_height) { + return; + } + + let coverage_source = vec2( + params.source_offset_x + local_x, + params.source_offset_y + local_y); + let coverage_value = textureLoad(coverage, coverage_source, 0).r; + let brush = sample_brush(params, destination_pixel_x, destination_pixel_y); + let source = vec4(brush.rgb, brush.a * coverage_value); + + let destination_index = (destination_pixel_y * params.destination_buffer_width) + destination_pixel_x; + let destination = destination_pixels[destination_index]; + destination_pixels[destination_index] = compose_pixel( + destination, + source, + params.blend_percentage, + params.color_blending_mode, + params.alpha_composition_mode); + } + """u8; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeShader.cs deleted file mode 100644 index 43f8ab1022cdb75e22c43809144bebbce779f445..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4162 zcmbtXZENF35bkIHiXl*ta*pCSM=5zR?&bO+5V+8A{ZfipOKW?nEQ!_1kvQ_-JG0uA zwX)>2M37kCotI~3o|#==Ucy6`*HUb@3cSdN7g50zTV=8cCYP7+BqE*`JccrjxdgT4 z9SL~J6QQh3Fqx!m$BUdrTF6*=+_4RR*)o|2UnP4L=_YuVS;UKiUca#DAD+g=?PMZS z#bwG8C=^p7f+%4{0b2cwEQ{?!w#&0ZC^CjvCO;;C4`QSZk?;#8b{wKCMg3I4iY&*l zs;#}}-yvp-ftO~m=F9%k!x!Btq@fViGdTT{mp^?(FMt|>$)JVq z{(JhbB39dXd~7)pZ$L;fxxpyLY?y6F-3e6?>q5ylW05!lL!meVV>lck2aPU?`PWFT zh{R!rttN((J0La$EX-Lv8%PN*ZSb^}E62)RfenO4rXaB&79e!XMribr z%I0=EG5xghxntb+fzIYBn5Mb@c|G^xVgV<@O1OfY zNwzECzRmru| zxQQahUOi~fW~edPOB1m=(~W-P^oQ%QkqB(3Hz{hSrl(_?0)5n)qf-Kf<)A9q#~82$ z(Vwn_LxE0B@aSne=qBg39lu^dL?= z=!+u_4tJC`AbU?pu_3Iwf2!OedY2S2j#ap5-r3>uB8Xh?P ztEszbt~1SyLEi>aqiOFt$B`HyI*2hKH8z%G&bh51*PTt{%QV{vl73DMC(XU#DPyr` zkm&%X&DnspUD$Pu-(sf|p;P2er!3camDurAwLOTvvo`0aNbyv6n_bg#Nj#%lE}hb+ zINF3woHKWn&Hfl@iCP^W$%7gl zYGc|RFT`zo4Gi^MLPXuF+D%(MDqSGm#ECWj+BA1bW905$-x+#?g5rur5iuFN7CICl z8A7Ewi(m%k))WhfRA@@lArh$)7OfgE_&5lYd6wi`hFGlO^M20GZA%^Q=Rt1_5irNx n$IzJuZ0T--mYWrxmR&!#aG?&OldZR(S|;Gn)biKc$=l>#=Mk+M diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs new file mode 100644 index 000000000..5518d502d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class SolidBrushCompositeComputeShader +{ + // Compile-time constant backed by static PE data (no heap allocation). + public static ReadOnlySpan Code => + """ + struct SolidBrushCompositeData { + source_offset_x: i32, + source_offset_y: i32, + destination_x: i32, + destination_y: i32, + destination_width: i32, + destination_height: i32, + destination_buffer_width: i32, + destination_buffer_height: i32, + blend_percentage: f32, + color_blending_mode: i32, + alpha_composition_mode: i32, + _common_pad0: i32, + solid_brush_color: vec4, + }; + + @group(0) @binding(0) + var coverage: texture_2d; + + @group(0) @binding(1) + var instance: SolidBrushCompositeData; + + @group(0) @binding(2) + var destination_pixels: array>; + + fn overlay_value(backdrop: f32, source: f32) -> f32 { + if (backdrop <= 0.5) { + return 2.0 * backdrop * source; + } + + return 1.0 - (2.0 * (1.0 - source) * (1.0 - backdrop)); + } + + fn blend_color(backdrop: vec3, source: vec3, color_mode: i32) -> vec3 { + switch color_mode { + case 0 { + return source; + } + + case 1 { + return backdrop * source; + } + + case 2 { + return min(vec3(1.0), backdrop + source); + } + + case 3 { + return max(vec3(0.0), backdrop - source); + } + + case 4 { + return vec3(1.0) - ((vec3(1.0) - backdrop) * (vec3(1.0) - source)); + } + + case 5 { + return min(backdrop, source); + } + + case 6 { + return max(backdrop, source); + } + + case 7 { + return vec3( + overlay_value(backdrop.r, source.r), + overlay_value(backdrop.g, source.g), + overlay_value(backdrop.b, source.b)); + } + + case 8 { + return vec3( + overlay_value(source.r, backdrop.r), + overlay_value(source.g, backdrop.g), + overlay_value(source.b, backdrop.b)); + } + + default { + return source; + } + } + } + + fn unpremultiply(premultiplied_rgb: vec3, alpha: f32) -> vec4 { + let clamped_alpha = clamp(alpha, 0.0, 1.0); + if (clamped_alpha <= 0.0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + let color = clamp( + premultiplied_rgb / clamped_alpha, + vec3(0.0), + vec3(1.0)); + return vec4(color, clamped_alpha); + } + + fn compose_over(destination: vec4, source: vec4, blend: vec3) -> vec4 { + let source_weight = source.a; + let destination_weight = destination.a; + let blend_weight = source_weight * destination_weight; + let destination_only_weight = destination_weight - blend_weight; + let source_only_weight = source_weight - blend_weight; + let alpha = destination_only_weight + source_weight; + let premultiplied_color = + (destination.rgb * destination_only_weight) + + (source.rgb * source_only_weight) + + (blend * blend_weight); + return unpremultiply(premultiplied_color, alpha); + } + + fn compose_atop(destination: vec4, source: vec4, blend: vec3) -> vec4 { + let source_weight = source.a; + let destination_weight = destination.a; + let blend_weight = source_weight * destination_weight; + let destination_only_weight = destination_weight - blend_weight; + let premultiplied_color = + (destination.rgb * destination_only_weight) + + (blend * blend_weight); + return unpremultiply(premultiplied_color, destination_weight); + } + + fn compose_in(destination: vec4, source: vec4) -> vec4 { + let alpha = destination.a * source.a; + return unpremultiply(source.rgb * alpha, alpha); + } + + fn compose_out(destination: vec4, source: vec4) -> vec4 { + let alpha = (1.0 - destination.a) * source.a; + return unpremultiply(source.rgb * alpha, alpha); + } + + fn compose_xor(destination: vec4, source: vec4) -> vec4 { + let source_weight = 1.0 - destination.a; + let destination_weight = 1.0 - source.a; + let alpha = (source.a * source_weight) + (destination.a * destination_weight); + let premultiplied_color = + (source.a * source.rgb * source_weight) + + (destination.a * destination.rgb * destination_weight); + return unpremultiply(premultiplied_color, alpha); + } + + fn compose_pixel( + destination: vec4, + source: vec4, + blend_percentage: f32, + color_mode: i32, + alpha_mode: i32) -> vec4 { + let source_alpha = clamp(source.a * blend_percentage, 0.0, 1.0); + let source_color = clamp(source.rgb, vec3(0.0), vec3(1.0)); + let source_with_opacity = vec4(source_color, source_alpha); + let destination_color = clamp(destination.rgb, vec3(0.0), vec3(1.0)); + let destination_alpha = clamp(destination.a, 0.0, 1.0); + let destination_pixel = vec4(destination_color, destination_alpha); + + switch alpha_mode { + case 0 { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_over(destination_pixel, source_with_opacity, blend); + } + + case 1 { + return source_with_opacity; + } + + case 2 { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_atop(destination_pixel, source_with_opacity, blend); + } + + case 3 { + return compose_in(destination_pixel, source_with_opacity); + } + + case 4 { + return compose_out(destination_pixel, source_with_opacity); + } + + case 5 { + return destination_pixel; + } + + case 6 { + let blend = blend_color(source_color, destination_color, color_mode); + return compose_atop(source_with_opacity, destination_pixel, blend); + } + + case 7 { + let blend = blend_color(source_color, destination_color, color_mode); + return compose_over(source_with_opacity, destination_pixel, blend); + } + + case 8 { + return compose_in(source_with_opacity, destination_pixel); + } + + case 9 { + return compose_out(source_with_opacity, destination_pixel); + } + + case 10 { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + case 11 { + return compose_xor(destination_pixel, source_with_opacity); + } + + default { + let blend = blend_color(destination_color, source_color, color_mode); + return compose_over(destination_pixel, source_with_opacity, blend); + } + } + } + + @compute @workgroup_size(8, 8, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + let params = instance; + let local_x = i32(global_id.x); + let local_y = i32(global_id.y); + if (local_x >= params.destination_width || local_y >= params.destination_height) { + return; + } + + let destination_pixel_x = params.destination_x + local_x; + let destination_pixel_y = params.destination_y + local_y; + if (destination_pixel_x < 0 || + destination_pixel_y < 0 || + destination_pixel_x >= params.destination_buffer_width || + destination_pixel_y >= params.destination_buffer_height) { + return; + } + + let coverage_source = vec2( + params.source_offset_x + local_x, + params.source_offset_y + local_y); + let coverage_value = textureLoad(coverage, coverage_source, 0).r; + let brush = params.solid_brush_color; + let source = vec4(brush.rgb, brush.a * coverage_value); + + let destination_index = (destination_pixel_y * params.destination_buffer_width) + destination_pixel_x; + let destination = destination_pixels[destination_index]; + destination_pixels[destination_index] = compose_pixel( + destination, + source, + params.blend_percentage, + params.color_blending_mode, + params.alpha_composition_mode); + } + """u8; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeShader.cs deleted file mode 100644 index cdcca6f1f57d5c2b589938eb58456b75c11d75e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3043 zcmbtWQETHk5WeU9ilI+-H+8e=u^ed{dfUB+LSf5=y_e%KvaKZI6v;?(YIF3z-$<5R zJ9XNH8WKCv%s2DRd>Z-k67FQ#D7M`hi1HZLtcC}YNu`s?um| zz`Eqj1e;_s5oAwwNpff8QSSF-OV>N1%H*3OPfTo+MzN+8rG7t;;X9k?oRGYshP20n9Jn93E+h}+6Bhxg<*RNxfJLx3}hC$fUF64 zkAFZx41q{U*iljf;auhzN{ToMSob35RY5fnKx$NBIzK+m@B&eB4*1WW31Xyz4SsAn z@E1E!c-oc~+`;dU_Yh@`q30g6tTK*dJf}Y?DZUHdtV<%6)@+5@7Zh$*E&)sE?Ch+2 z)<#vi8LPoJ%cQM*hwi{Un7qColLbGLdR3dMIo$!zHYg;JYJ(R5z8&~KxB9-u5W$_qiJpinI< zU{26Ef{yG3tAif67tCK&VFKSPQkCAV3{IQBSld&z1a!qaF0Ecx58Pb_!)%$@1{ zsH%fnVIeSut~wh52c7N-2JZfI&>@}HKE=AyI>w$^@AlUMs;hYn7b|#!7N-VEqR3vu zO;@q&K`7fGi;EM@8WE*PvqW)t8t`Q`OwFd(El0CtHU<2OhbkHxp~|8*i=@9(oQmW3 z-Cc|m7_HRfH^YC_|3v7YP5&MD6U%{E+>^1bs_m%L8ulu*?eD%W{u#E{DS*|KJA2>N z=tR&OyK!QviDUTGMt&VY6nR=>fT*;Qk{T}iF#Vy0DCaOwW^e(Yli4^@gGjiDCLXUD z#S0Z`0guXLRW02{UkvugFk)+wgq_}&sGW+D<1__(Yn(=>HYsd}?LxRbKxl}&4=ohM zfhT+}FH6t8`MJTdwx_~$$T$@?ekw45vua4e?fJp%=|=g{Q2UA9-^t1K50A!Z!=+S_ zi<>w;NgW@tesQv`8h1jyys)+9vQF(;+?IL1M8}cY;s?diu^qIR)~^T6s7KRzG-F#R z97*Yc@Csiv!pZa?NfCv`sZSW&A17)^hi&k^(4T7_WO?Hz;ZYMT_k!u1sA7oFlR&7C zJ9aUdj?vLsJOr>QIFE$D$7B0~6JG8JrVxP7yVKjVJP+Buyi6qZ>%<0dAIr{fp-$zV fwAyC4_4{!`zfwn{r@!Z4y1j+(k=1Y4ljq6*%#T~Q diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 3ade0cc67..11587f069 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -22,6 +21,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; + private const int CompositeComputeWorkgroupSize = 8; + private const int CompositeDestinationPixelStride = 16; private const nuint CompositeInstanceBufferSize = 256 * 1024; private const int CallbackTimeoutMilliseconds = 10_000; @@ -152,15 +153,8 @@ public void FlushCompositions( this.TestingCompositeCoverageCallCount += commandCount; bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); - if (!AreAllCompositionBrushesSupported(compositionBatch.Commands)) + if (compositionBatch.FlushId == 0 && !AreAllCompositionBrushesSupported(compositionBatch.Commands)) { - if (compositionBatch.FlushId != 0) - { - throw new InvalidOperationException( - "Unsupported brush reached a shared WebGPU flush session. " + - "Flush-time brush support validation should have prevented this."); - } - this.TestingFallbackPrepareCoverageCallCount++; this.TestingFallbackCompositeCoverageCallCount += commandCount; this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); @@ -201,7 +195,12 @@ public void FlushCompositions( out failure)) { gpuReady = true; - gpuSuccess = this.TryCompositeBatch(flushContext, coverageEntry, target.Bounds, compositionBatch.Commands, out failure); + gpuSuccess = this.TryCompositeBatch( + flushContext, + coverageEntry, + compositionBatch.Commands, + blitToTarget: !useFlushSession || compositionBatch.IsFinalBatchInFlush, + out failure); if (gpuSuccess) { if (useFlushSession && !compositionBatch.IsFinalBatchInFlush) @@ -272,8 +271,7 @@ public void FlushCompositions( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) - where TPixel : unmanaged, IPixel + private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) { for (int i = 0; i < commands.Count; i++) { @@ -335,8 +333,8 @@ private static bool TryPrepareGpuCoverage( private bool TryCompositeBatch( WebGPUFlushContext flushContext, WebGPUFlushContext.CoverageEntry coverageEntry, - in Rectangle destinationBounds, IReadOnlyList commands, + bool blitToTarget, out string? error) where TPixel : unmanaged, IPixel { @@ -347,28 +345,41 @@ private bool TryCompositeBatch( return true; } + Rectangle targetLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); + if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) + { + return true; + } + IWebGPUBrushComposer[] composers = new IWebGPUBrushComposer[commandCount]; + bool hasPreviousComposer = false; + WebGPUBrushComposerCacheKey previousComposerKey = default; + IWebGPUBrushComposer? previousComposer = null; for (int i = 0; i < composers.Length; i++) { - composers[i] = WebGPUBrushComposerFactory.Create(flushContext, commands[i]); + PreparedCompositionCommand command = commands[i]; + WebGPUBrushComposerCacheKey cacheKey = WebGPUBrushComposerFactory.CreateCacheKey(command); + IWebGPUBrushComposer? composer; + if (hasPreviousComposer && cacheKey.Equals(previousComposerKey)) + { + composer = previousComposer!; + } + else + { + composer = WebGPUBrushComposerFactory.Create(flushContext, command); + } + + composers[i] = composer!; + previousComposerKey = cacheKey; + previousComposer = composer!; + hasPreviousComposer = true; } nuint totalInstanceBytes = 0; - nuint maxInstanceBytes = 0; for (int i = 0; i < composers.Length; i++) { nuint instanceBytes = composers[i].InstanceDataSizeInBytes; - if (instanceBytes == 0) - { - error = "Brush composer returned an empty instance payload."; - return false; - } - totalInstanceBytes = checked(totalInstanceBytes + AlignToStorageBufferOffset(instanceBytes)); - if (instanceBytes > maxInstanceBytes) - { - maxInstanceBytes = instanceBytes; - } } nuint instanceOffset = flushContext.InstanceBufferWriteOffset; @@ -390,80 +401,590 @@ private bool TryCompositeBatch( } if (!flushContext.EnsureInstanceBufferCapacity(requiredCapacity, Math.Max(requiredCapacity, CompositeInstanceBufferSize)) || - !flushContext.EnsureCommandEncoder() || - !flushContext.BeginRenderPass()) + !flushContext.EnsureCommandEncoder()) + { + error = "Failed to allocate WebGPU composition buffers."; + return false; + } + + if (flushContext.TargetTexture is null || flushContext.TargetView is null) { - error = "Failed to allocate WebGPU composition buffers or begin render pass."; + error = "WebGPU flush context does not expose a target texture/view."; + return false; + } + + WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; + nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; + if (destinationPixelsBuffer is null) + { + // Initialize the destination buffer once per flush from the current target texture. + TextureView* sourceTextureView = flushContext.TargetView; + if (!flushContext.CanSampleTargetTexture) + { + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out Texture* sourceTexture, + out sourceTextureView, + out error)) + { + return false; + } + + CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); + } + + if (!TryCreateDestinationPixelsBuffer( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out destinationPixelsBuffer, + out destinationPixelsByteSize, + out error) || + !TryInitializeDestinationPixels( + flushContext, + sourceTextureView, + destinationPixelsBuffer, + targetLocalBounds.Width, + targetLocalBounds.Height, + destinationPixelsByteSize, + out error)) + { + return false; + } + + flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; + flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; + } + + Span instanceScratch = flushContext.GetCompositionInstanceScratchBuffer(checked((int)totalInstanceBytes)); + nuint localInstanceOffset = 0; + int destinationBufferWidth = targetLocalBounds.Width; + int destinationBufferHeight = targetLocalBounds.Height; + for (int i = 0; i < composers.Length; i++) + { + IWebGPUBrushComposer composer = composers[i]; + PreparedCompositionCommand command = commands[i]; + nuint instanceBytes = composer.InstanceDataSizeInBytes; + int instanceBytesInt = checked((int)instanceBytes); + int destinationX = command.DestinationRegion.X - flushContext.TargetBounds.X; + int destinationY = command.DestinationRegion.Y - flushContext.TargetBounds.Y; + WebGPUCompositeCommonParameters common = new( + command.SourceOffset.X, + command.SourceOffset.Y, + destinationX, + destinationY, + command.DestinationRegion.Width, + command.DestinationRegion.Height, + destinationBufferWidth, + destinationBufferHeight, + 0, + 0, + command.GraphicsOptions.BlendPercentage, + (int)command.GraphicsOptions.ColorBlendingMode, + (int)command.GraphicsOptions.AlphaCompositionMode); + + Span payload = instanceScratch.Slice(checked((int)localInstanceOffset), instanceBytesInt); + composer.WriteInstanceData(in common, payload); + localInstanceOffset = checked(localInstanceOffset + AlignToStorageBufferOffset(instanceBytes)); + } + + fixed (byte* payloadPtr = instanceScratch) + { + // Upload all instance payloads in one call to minimize queue write overhead. + flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, instanceOffset, payloadPtr, totalInstanceBytes); + } + + if (!TryRunCompositeCommandComputePass( + flushContext, + coverageEntry.GPUCoverageView, + destinationPixelsBuffer, + destinationPixelsByteSize, + commands, + composers, + instanceOffset, + out nuint finalCommandOffset, + out error)) + { + return false; + } + + if (blitToTarget && + !TryBlitDestinationPixelsToTarget( + flushContext, + destinationPixelsBuffer, + destinationPixelsByteSize, + targetLocalBounds, + out error)) + { + return false; + } + + flushContext.AdvanceInstanceBufferOffset(finalCommandOffset); + return true; + } + + private static bool TryCreateDestinationPixelsBuffer( + WebGPUFlushContext flushContext, + int width, + int height, + out WgpuBuffer* destinationPixelsBuffer, + out nuint destinationPixelsByteSize, + out string? error) + { + destinationPixelsByteSize = checked((nuint)width * (nuint)height * CompositeDestinationPixelStride); + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage, + Size = destinationPixelsByteSize + }; + + destinationPixelsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (destinationPixelsBuffer is null) + { + error = "Failed to create destination pixel storage buffer."; + return false; + } + + flushContext.TrackBuffer(destinationPixelsBuffer); + error = null; + return true; + } + + private static bool TryInitializeDestinationPixels( + WebGPUFlushContext flushContext, + TextureView* sourceTextureView, + WgpuBuffer* destinationPixelsBuffer, + int destinationWidth, + int destinationHeight, + nuint destinationPixelsByteSize, + out string? error) + { + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + "composite-destination-init", + CompositeDestinationInitShader.Code, + TryCreateDestinationInitBindGroupLayout, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = sourceTextureView + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = destinationPixelsBuffer, + Offset = 0, + Size = destinationPixelsByteSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 2, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create destination initialization bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin destination initialization compute pass."; + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + uint dispatchX = DivideRoundUp(destinationWidth, CompositeComputeWorkgroupSize); + uint dispatchY = DivideRoundUp(destinationHeight, CompositeComputeWorkgroupSize); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, dispatchX, dispatchY, 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + error = null; + return true; + } + + private static bool TryRunCompositeCommandComputePass( + WebGPUFlushContext flushContext, + TextureView* coverageView, + WgpuBuffer* destinationPixelsBuffer, + nuint destinationPixelsByteSize, + IReadOnlyList commands, + IWebGPUBrushComposer[] composers, + nuint instanceOffset, + out nuint finalCommandOffset, + out string? error) + { + finalCommandOffset = instanceOffset; + error = null; + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin WebGPU composition compute pass."; return false; } - byte[]? rentedInstanceData = null; try { - rentedInstanceData = ArrayPool.Shared.Rent(checked((int)maxInstanceBytes)); - Span instanceScratch = rentedInstanceData; nuint commandOffset = instanceOffset; - int targetWidth = flushContext.TargetBounds.Width; - int targetHeight = flushContext.TargetBounds.Height; + IWebGPUBrushComposer? previousComposer = null; + ComputePipeline* previousComposerPipeline = null; + ComputePipeline* currentBoundPipeline = null; for (int i = 0; i < composers.Length; i++) { IWebGPUBrushComposer composer = composers[i]; PreparedCompositionCommand command = commands[i]; nuint instanceBytes = composer.InstanceDataSizeInBytes; - int instanceBytesInt = checked((int)instanceBytes); - int destinationX = destinationBounds.X + command.DestinationRegion.X - flushContext.TargetBounds.X; - int destinationY = destinationBounds.Y + command.DestinationRegion.Y - flushContext.TargetBounds.Y; - WebGPUCompositeCommonParameters common = new( - command.SourceOffset.X, - command.SourceOffset.Y, - destinationX, - destinationY, - command.DestinationRegion.Width, - command.DestinationRegion.Height, - targetWidth, - targetHeight, - command.GraphicsOptions.BlendPercentage); - - Span payload = instanceScratch[..instanceBytesInt]; - composer.WriteInstanceData(in common, payload); - - fixed (byte* payloadPtr = payload) + ComputePipeline* pipeline; + if (ReferenceEquals(composer, previousComposer)) { - // QueueWriteBuffer copies source bytes into driver-owned staging immediately. - flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, commandOffset, payloadPtr, instanceBytes); + pipeline = previousComposerPipeline; } - - if (!composer.TryGetOrCreatePipeline(flushContext, out RenderPipeline* pipeline, out string? pipelineError)) + else if (!composer.TryGetOrCreatePipeline(flushContext, out pipeline, out string? pipelineError)) { - error = pipelineError ?? "Failed to create composite pipeline."; + error = pipelineError ?? "Failed to create composite compute pipeline."; return false; } BindGroup* bindGroup = composer.CreateBindGroup( flushContext, - coverageEntry.GPUCoverageView, + coverageView, + destinationPixelsBuffer, + destinationPixelsByteSize, commandOffset, instanceBytes); - flushContext.TrackBindGroup(bindGroup); - flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); - flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); - flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, 0); + if (pipeline != currentBoundPipeline) + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + currentBoundPipeline = pipeline; + } + + uint dynamicOffset = checked((uint)commandOffset); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 1, &dynamicOffset); + uint dispatchX = DivideRoundUp(command.DestinationRegion.Width, CompositeComputeWorkgroupSize); + uint dispatchY = DivideRoundUp(command.DestinationRegion.Height, CompositeComputeWorkgroupSize); + if (dispatchX > 0 && dispatchY > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, dispatchX, dispatchY, 1); + } + commandOffset = checked(commandOffset + AlignToStorageBufferOffset(instanceBytes)); + previousComposer = composer; + previousComposerPipeline = pipeline; } - flushContext.AdvanceInstanceBufferOffset(commandOffset); + finalCommandOffset = commandOffset; return true; } finally { - if (rentedInstanceData is not null) + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + } + + private static bool TryBlitDestinationPixelsToTarget( + WebGPUFlushContext flushContext, + WgpuBuffer* destinationPixelsBuffer, + nuint destinationPixelsByteSize, + in Rectangle destinationBounds, + out string? error) + { + if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline( + "composite-destination-blit", + CompositeDestinationBlitShader.Code, + TryCreateDestinationBlitBindGroupLayout, + flushContext.TextureFormat, + out BindGroupLayout* bindGroupLayout, + out RenderPipeline* pipeline, + out error)) + { + return false; + } + + BufferDescriptor paramsDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = 16 + }; + WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); + if (paramsBuffer is null) + { + error = "Failed to create destination blit parameter buffer."; + return false; + } + + flushContext.TrackBuffer(paramsBuffer); + CompositeDestinationBlitParameters parameters = new( + destinationBounds.Width, + destinationBounds.Height, + destinationBounds.X, + destinationBounds.Y); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + paramsBuffer, + 0, + ¶meters, + (nuint)Unsafe.SizeOf()); + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + Buffer = destinationPixelsBuffer, + Offset = 0, + Size = destinationPixelsByteSize + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = paramsBuffer, + Offset = 0, + Size = (nuint)Unsafe.SizeOf() + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 2, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create destination blit bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + if (!flushContext.BeginRenderPass(flushContext.TargetView)) + { + error = "Failed to begin destination blit render pass."; + return false; + } + + flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); + flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderSetScissorRect( + flushContext.PassEncoder, + (uint)destinationBounds.X, + (uint)destinationBounds.Y, + (uint)destinationBounds.Width, + (uint)destinationBounds.Height); + flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, 0); + flushContext.EndRenderPassIfOpen(); + error = null; + return true; + } + + private static bool TryCreateDestinationInitBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { - ArrayPool.Shared.Return(rentedInstanceData); + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create destination init bind group layout."; + return false; } + + error = null; + return true; } + private static bool TryCreateDestinationBlitBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Fragment, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create destination blit bind group layout."; + return false; + } + + error = null; + return true; + } + + ///

+ /// Creates one transient composition texture that can be rendered to, sampled from, and copied. + /// + private static bool TryCreateCompositionTexture( + WebGPUFlushContext flushContext, + int width, + int height, + out Texture* texture, + out TextureView* textureView, + out string? error) + { + texture = null; + textureView = null; + + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = flushContext.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in textureDescriptor); + if (texture is null) + { + error = "Failed to create WebGPU composition texture."; + return false; + } + + TextureViewDescriptor textureViewDescriptor = new() + { + Format = flushContext.TextureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + texture = null; + error = "Failed to create WebGPU composition texture view."; + return false; + } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CopyTextureRegion( + WebGPUFlushContext flushContext, + Texture* sourceTexture, + Texture* destinationTexture, + in Rectangle sourceRegion) + { + ImageCopyTexture source = new() + { + Texture = sourceTexture, + MipLevel = 0, + Origin = new Origin3D((uint)sourceRegion.X, (uint)sourceRegion.Y, 0), + Aspect = TextureAspect.All + }; + + ImageCopyTexture destination = new() + { + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + Extent3D copySize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + flushContext.Api.CommandEncoderCopyTextureToTexture(flushContext.CommandEncoder, in source, in destination, in copySize); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint DivideRoundUp(int value, int divisor) + => (uint)((value + divisor - 1) / divisor); + private bool TryFinalizeFlush( WebGPUFlushContext flushContext, Buffer2DRegion cpuRegion) @@ -718,4 +1239,20 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.IsSet; } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct CompositeDestinationBlitParameters( + int batchWidth, + int batchHeight, + int targetOriginX, + int targetOriginY) + { + public readonly int BatchWidth = batchWidth; + + public readonly int BatchHeight = batchHeight; + + public readonly int TargetOriginX = targetOriginX; + + public readonly int TargetOriginY = targetOriginY; + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 168ac909b..e2344d8a9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -29,7 +29,9 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable private bool ownsTargetTexture; private bool ownsTargetView; private bool ownsReadbackBuffer; + private byte[]? compositionInstanceScratchBuffer; private readonly List transientBindGroups = []; + private readonly List transientBuffers = []; private readonly List transientTextureViews = []; private readonly List transientTextures = []; @@ -75,6 +77,11 @@ private WebGPUFlushContext( public bool RequiresReadback { get; private set; } + /// + /// Gets a value indicating whether the current target texture can be sampled in a compute shader. + /// + public bool CanSampleTargetTexture { get; private set; } + public WgpuBuffer* ReadbackBuffer { get; private set; } public uint ReadbackBytesPerRow { get; private set; } @@ -87,6 +94,17 @@ private WebGPUFlushContext( public nuint InstanceBufferWriteOffset { get; internal set; } + /// + /// Gets or sets the flush-scoped destination pixel buffer used by composition compute shaders. + /// This buffer is initialized once per flush from the target texture and reused across composition batches. + /// + public WgpuBuffer* CompositeDestinationPixelsBuffer { get; internal set; } + + /// + /// Gets or sets the byte size of . + /// + public nuint CompositeDestinationPixelsByteSize { get; internal set; } + public CommandEncoder* CommandEncoder { get; set; } public RenderPassEncoder* PassEncoder { get; private set; } @@ -311,20 +329,28 @@ public bool EnsureCommandEncoder() } public bool BeginRenderPass() + { + return this.BeginRenderPass(this.TargetView); + } + + /// + /// Begins a render pass that targets the specified texture view. + /// + public bool BeginRenderPass(TextureView* targetView) { if (this.PassEncoder is not null) { return true; } - if (this.CommandEncoder is null || this.TargetView is null) + if (this.CommandEncoder is null || targetView is null) { return false; } RenderPassColorAttachment colorAttachment = new() { - View = this.TargetView, + View = targetView, ResolveTarget = null, LoadOp = LoadOp.Load, StoreOp = StoreOp.Store, @@ -361,6 +387,17 @@ public void TrackBindGroup(BindGroup* bindGroup) } } + /// + /// Tracks a transient buffer allocated during this flush. + /// + public void TrackBuffer(WgpuBuffer* buffer) + { + if (buffer is not null) + { + this.transientBuffers.Add((nint)buffer); + } + } + /// /// Tracks a transient texture view allocated during this flush. /// @@ -383,6 +420,31 @@ public void TrackTexture(Texture* texture) } } + /// + /// Gets a flush-scoped scratch buffer for writing composition instance payload bytes. + /// + public Span GetCompositionInstanceScratchBuffer(int requiredLength) + { + if (requiredLength <= 0) + { + return Span.Empty; + } + + byte[]? current = this.compositionInstanceScratchBuffer; + if (current is null || current.Length < requiredLength) + { + if (current is not null) + { + ArrayPool.Shared.Return(current); + } + + this.compositionInstanceScratchBuffer = ArrayPool.Shared.Rent(requiredLength); + current = this.compositionInstanceScratchBuffer; + } + + return current.AsSpan(0, requiredLength); + } + /// /// Gets a texture view for the source image from this flush cache, creating and uploading it on first use. /// @@ -496,6 +558,11 @@ public void Dispose() this.Api.BindGroupRelease((BindGroup*)this.transientBindGroups[i]); } + for (int i = 0; i < this.transientBuffers.Count; i++) + { + this.Api.BufferRelease((WgpuBuffer*)this.transientBuffers[i]); + } + for (int i = 0; i < this.transientTextureViews.Count; i++) { this.Api.TextureViewRelease((TextureView*)this.transientTextureViews[i]); @@ -507,18 +574,28 @@ public void Dispose() } this.transientBindGroups.Clear(); + this.transientBuffers.Clear(); this.transientTextureViews.Clear(); this.transientTextures.Clear(); + if (this.compositionInstanceScratchBuffer is not null) + { + ArrayPool.Shared.Return(this.compositionInstanceScratchBuffer); + this.compositionInstanceScratchBuffer = null; + } + // Cache entries point to transient texture views that are released above. this.cachedSourceTextureViews.Clear(); this.ReadbackBuffer = null; this.TargetView = null; this.TargetTexture = null; + this.CompositeDestinationPixelsBuffer = null; + this.CompositeDestinationPixelsByteSize = 0; this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; this.RequiresReadback = false; + this.CanSampleTargetTexture = false; this.ownsReadbackBuffer = false; this.ownsTargetView = false; this.ownsTargetTexture = false; @@ -714,6 +791,7 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability) this.TargetTexture = (Texture*)capability.TargetTexture; this.TargetView = (TextureView*)capability.TargetTextureView; this.RequiresReadback = false; + this.CanSampleTargetTexture = capability.SupportsTextureSampling; this.ReadbackBuffer = null; this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; @@ -733,7 +811,7 @@ private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int p TextureDescriptor targetTextureDescriptor = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, Dimension = TextureDimension.Dimension2D, Size = new Extent3D((uint)width, (uint)height, 1), Format = this.TextureFormat, @@ -797,6 +875,7 @@ private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int p this.ReadbackBytesPerRow = readbackRowBytes; this.ReadbackByteCount = readbackByteCount; this.RequiresReadback = true; + this.CanSampleTargetTexture = true; this.ownsTargetTexture = true; this.ownsTargetView = true; this.ownsReadbackBuffer = true; @@ -945,6 +1024,7 @@ internal sealed class DeviceSharedState : IDisposable { private readonly Dictionary coverageCache = []; private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); private WebGPURasterizer? coverageRasterizer; private bool disposed; @@ -958,6 +1038,8 @@ internal DeviceSharedState(WebGPU api, Device* device) private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; + private static ReadOnlySpan CompositeComputeEntryPoint => "cs_main\0"u8; + public object SyncRoot { get; } = new(); public WebGPU Api { get; } @@ -1110,6 +1192,85 @@ infrastructure.PipelineLayout is null || } } + public bool TryGetOrCreateCompositeComputePipeline( + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out string? error) + { + bindGroupLayout = null; + pipeline = null; + + if (this.disposed) + { + error = "WebGPU device state is disposed."; + return false; + } + + if (string.IsNullOrWhiteSpace(pipelineKey)) + { + error = "Composite compute pipeline key cannot be empty."; + return false; + } + + if (shaderCode.IsEmpty) + { + error = $"Composite compute shader code is missing for pipeline '{pipelineKey}'."; + return false; + } + + CompositeComputePipelineInfrastructure infrastructure = this.compositeComputePipelines.GetOrAdd( + pipelineKey, + static _ => new CompositeComputePipelineInfrastructure()); + + lock (infrastructure) + { + if (infrastructure.BindGroupLayout is null || + infrastructure.PipelineLayout is null || + infrastructure.ShaderModule is null) + { + if (!this.TryCreateCompositeInfrastructure( + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* createdBindGroupLayout, + out PipelineLayout* createdPipelineLayout, + out ShaderModule* createdShaderModule, + out error)) + { + return false; + } + + infrastructure.BindGroupLayout = createdBindGroupLayout; + infrastructure.PipelineLayout = createdPipelineLayout; + infrastructure.ShaderModule = createdShaderModule; + } + + bindGroupLayout = infrastructure.BindGroupLayout; + if (infrastructure.Pipeline is not null) + { + pipeline = infrastructure.Pipeline; + error = null; + return true; + } + + ComputePipeline* createdPipeline = this.CreateCompositeComputePipeline( + infrastructure.PipelineLayout, + infrastructure.ShaderModule); + if (createdPipeline is null) + { + error = $"Failed to create composite compute pipeline '{pipelineKey}'."; + return false; + } + + infrastructure.Pipeline = createdPipeline; + pipeline = createdPipeline; + error = null; + return true; + } + } + public void Dispose() { if (this.disposed) @@ -1134,6 +1295,13 @@ public void Dispose() this.compositePipelines.Clear(); + foreach (CompositeComputePipelineInfrastructure infrastructure in this.compositeComputePipelines.Values) + { + this.ReleaseCompositeComputeInfrastructure(infrastructure); + } + + this.compositeComputePipelines.Clear(); + this.disposed = true; } @@ -1170,23 +1338,7 @@ private bool TryCreateCompositeInfrastructure( return false; } - // The native wgpu C API expects a null-terminated byte* for Code. - // Shader spans include the \0 terminator so fixed pinning is sufficient. - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); - } + shaderModule = this.CreateShaderModule(shaderCode); if (shaderModule is null) { @@ -1236,27 +1388,11 @@ private bool TryCreateCompositeInfrastructure( Buffers = null }; - BlendState blendState = new() - { - Color = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - }, - Alpha = new BlendComponent - { - Operation = BlendOperation.Add, - SrcFactor = BlendFactor.One, - DstFactor = BlendFactor.OneMinusSrcAlpha - } - }; - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; colorTargets[0] = new ColorTargetState { Format = textureFormat, - Blend = &blendState, + Blend = null, WriteMask = ColorWriteMask.All }; @@ -1292,6 +1428,52 @@ private bool TryCreateCompositeInfrastructure( return this.Api.DeviceCreateRenderPipeline(this.Device, in descriptor); } + private ComputePipeline* CreateCompositeComputePipeline( + PipelineLayout* pipelineLayout, + ShaderModule* shaderModule) + { + ReadOnlySpan entryPoint = CompositeComputeEntryPoint; + fixed (byte* entryPointPtr = entryPoint) + { + ProgrammableStageDescriptor computeState = new() + { + Module = shaderModule, + EntryPoint = entryPointPtr + }; + + ComputePipelineDescriptor descriptor = new() + { + Layout = pipelineLayout, + Compute = computeState + }; + + return this.Api.DeviceCreateComputePipeline(this.Device, in descriptor); + } + } + + private ShaderModule* CreateShaderModule(ReadOnlySpan shaderCode) + { + System.Diagnostics.Debug.Assert( + !shaderCode.IsEmpty && shaderCode[^1] == 0, + "WGSL shader code must be null-terminated at the call site."); + + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + return this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); + } + } + private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infrastructure) { foreach (nint pipelineHandle in infrastructure.Pipelines.Values) @@ -1323,6 +1505,33 @@ private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infr } } + private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfrastructure infrastructure) + { + if (infrastructure.Pipeline is not null) + { + this.Api.ComputePipelineRelease(infrastructure.Pipeline); + infrastructure.Pipeline = null; + } + + if (infrastructure.PipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(infrastructure.PipelineLayout); + infrastructure.PipelineLayout = null; + } + + if (infrastructure.ShaderModule is not null) + { + this.Api.ShaderModuleRelease(infrastructure.ShaderModule); + infrastructure.ShaderModule = null; + } + + if (infrastructure.BindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(infrastructure.BindGroupLayout); + infrastructure.BindGroupLayout = null; + } + } + private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) { if (entry.GPUCoverageView is not null) @@ -1348,6 +1557,17 @@ private sealed class CompositePipelineInfrastructure public ShaderModule* ShaderModule { get; set; } } + + private sealed class CompositeComputePipelineInfrastructure + { + public BindGroupLayout* BindGroupLayout { get; set; } + + public PipelineLayout* PipelineLayout { get; set; } + + public ShaderModule* ShaderModule { get; set; } + + public ComputePipeline* Pipeline { get; set; } + } } internal sealed class CoverageEntry diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs index 130875fc9..2b9878c0f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs @@ -23,6 +23,9 @@ public static class WebGPUNativeSurfaceFactory /// Surface height in pixels. /// Whether the surface is sRGB encoded. /// Whether surface alpha is premultiplied. + /// + /// Whether supports texture sampling. + /// /// A configured instance. public static NativeSurface Create( nint deviceHandle, @@ -33,7 +36,8 @@ public static NativeSurface Create( int width, int height, bool isSrgb, - bool isPremultipliedAlpha) + bool isPremultipliedAlpha, + bool supportsTextureSampling) where TPixel : unmanaged, IPixel { ValidateCommon( @@ -55,7 +59,8 @@ public static NativeSurface Create( width, height, isSrgb, - isPremultipliedAlpha)); + isPremultipliedAlpha, + supportsTextureSampling)); return nativeSurface; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs index 43729ff48..4c8f78d66 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -20,6 +20,9 @@ public sealed class WebGPUSurfaceCapability /// Surface height in pixels. /// Whether the target format is sRGB encoded. /// Whether alpha is premultiplied in the target surface. + /// + /// Whether can be sampled as a texture binding. + /// public WebGPUSurfaceCapability( nint device, nint queue, @@ -29,7 +32,8 @@ public WebGPUSurfaceCapability( int width, int height, bool isSrgb, - bool isPremultipliedAlpha) + bool isPremultipliedAlpha, + bool supportsTextureSampling) { this.Device = device; this.Queue = queue; @@ -40,6 +44,7 @@ public WebGPUSurfaceCapability( this.Height = height; this.IsSrgb = isSrgb; this.IsPremultipliedAlpha = isPremultipliedAlpha; + this.SupportsTextureSampling = supportsTextureSampling; } /// @@ -86,4 +91,9 @@ public WebGPUSurfaceCapability( /// Gets a value indicating whether the target uses premultiplied alpha. /// public bool IsPremultipliedAlpha { get; } + + /// + /// Gets a value indicating whether the target texture supports texture sampling. + /// + public bool SupportsTextureSampling { get; } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs index 61f5bb213..b0589520d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -57,7 +57,7 @@ internal static bool TryCreate( TextureDescriptor targetTextureDescriptor = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst, + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, Dimension = TextureDimension.Dimension2D, Size = new Extent3D((uint)width, (uint)height, 1), Format = textureFormat, @@ -108,7 +108,8 @@ internal static bool TryCreate( width, height, isSrgb, - isPremultipliedAlpha); + isPremultipliedAlpha, + supportsTextureSampling: true); error = string.Empty; return true; } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 1de55e78e..0e31370f0 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -14,6 +14,20 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; [GroupOutput("Drawing")] public class WebGPUDrawingBackendTests { + private static readonly (PixelColorBlendingMode ColorMode, PixelAlphaCompositionMode AlphaMode)[] GraphicsOptionsModePairs = + [ + (PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver), + (PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop), + (PixelColorBlendingMode.Add, PixelAlphaCompositionMode.Src), + (PixelColorBlendingMode.Subtract, PixelAlphaCompositionMode.DestOut), + (PixelColorBlendingMode.Screen, PixelAlphaCompositionMode.DestOver), + (PixelColorBlendingMode.Darken, PixelAlphaCompositionMode.DestAtop), + (PixelColorBlendingMode.Lighten, PixelAlphaCompositionMode.DestIn), + (PixelColorBlendingMode.Overlay, PixelAlphaCompositionMode.SrcIn), + (PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.Xor), + (PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Clear) + ]; + [Theory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) @@ -205,6 +219,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test "FillPath_NonZeroNestedContours_Expected", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + webGpuImage.CompareToReferenceOutput( referenceComparer, provider, @@ -216,6 +231,104 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test comparer.VerifySimilarity(defaultImage, webGpuImage); } + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); + Brush brush = Brushes.Solid(Color.OrangeRed.WithAlpha(0.78F)); + ImageComparer comparer = ImageComparer.TolerantPercentage(0.1F); + for (int i = 0; i < GraphicsOptionsModePairs.Length; i++) + { + (PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) = GraphicsOptionsModePairs[i]; + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + defaultImage.DebugSave( + provider, + $"DefaultBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = baseImage.Clone(); + using WebGPUDrawingBackend backend = new(); + Configuration webGpuConfiguration = Configuration.Default.Clone(); + webGpuConfiguration.SetDrawingBackend(backend); + webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); + webGpuImage.DebugSave( + provider, + $"WebGPUBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + } + + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); + ImageComparer comparer = ImageComparer.TolerantPercentage(0.1F); + for (int i = 0; i < GraphicsOptionsModePairs.Length; i++) + { + (PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) = GraphicsOptionsModePairs[i]; + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + defaultImage.DebugSave( + provider, + $"DefaultBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = baseImage.Clone(); + using WebGPUDrawingBackend backend = new(); + Configuration webGpuConfiguration = Configuration.Default.Clone(); + webGpuConfiguration.SetDrawingBackend(backend); + webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); + webGpuImage.DebugSave( + provider, + $"WebGPUBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + comparer.VerifySimilarity(defaultImage, webGpuImage); + } + } + [Theory] [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) From f659a3a541bc739dc33cbeea6efc08604a2a32d4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 25 Feb 2026 00:10:08 +1000 Subject: [PATCH 021/136] Tiled composite compute pass and brush refactor --- .../Brushes/IWebGPUBrushComposer.cs | 55 -- .../Brushes/WebGPUBrushComposerCacheKey.cs | 44 - .../Brushes/WebGPUBrushComposerFactory.cs | 56 -- .../WebGPUCompositeCommonParameters.cs | 66 -- .../WebGPUImageBrushComposer{TPixel}.cs | 289 ------ .../Brushes/WebGPUSolidBrushComposer.cs | 223 ----- .../SolidBrushCompositeComputeShader.cs | 260 ------ ...ader.cs => TiledCompositeComputeShader.cs} | 148 +++- .../WebGPUDrawingBackend.TiledComposite.cs | 823 ++++++++++++++++++ .../WebGPUDrawingBackend.cs | 293 +------ .../WebGPUFlushContext.cs | 39 +- .../Extensions/GraphicsOptionsExtensions.cs | 4 +- .../DrawingCanvasBatcher{TPixel}.cs | 5 +- .../Backends/WebGPUDrawingBackendTests.cs | 247 ++++-- 14 files changed, 1139 insertions(+), 1413 deletions(-) delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs rename src/ImageSharp.Drawing.WebGPU/Shaders/{ImageBrushCompositeComputeShader.cs => TiledCompositeComputeShader.cs} (65%) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs deleted file mode 100644 index 7907810a9..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/IWebGPUBrushComposer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using Silk.NET.WebGPU; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// Defines brush-specific GPU composition behavior. -/// -internal unsafe interface IWebGPUBrushComposer -{ - /// - /// Gets the size in bytes of this composer's instance payload. - /// - public nuint InstanceDataSizeInBytes { get; } - - /// - /// Gets or creates the compute pipeline required by this brush composer. - /// - /// The active WebGPU flush context. - /// The created or cached compute pipeline. - /// The error message when pipeline acquisition fails. - /// if the pipeline is available; otherwise . - public bool TryGetOrCreatePipeline( - WebGPUFlushContext flushContext, - out ComputePipeline* pipeline, - out string? error); - - /// - /// Writes one brush-specific instance payload into . - /// - /// The command values shared by every brush payload. - /// The destination bytes for the payload. - public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination); - - /// - /// Creates the bind group for this brush using the current coverage and destination buffers. - /// - /// The active WebGPU flush context. - /// The coverage texture view for the current batch. - /// The storage buffer containing destination pixels. - /// The byte size of . - /// The instance buffer offset. - /// The bound instance byte length. - /// The created bind group. - public BindGroup* CreateBindGroup( - WebGPUFlushContext flushContext, - TextureView* coverageView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - nuint instanceOffset, - nuint instanceBytes); -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs deleted file mode 100644 index 5c615a74a..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerCacheKey.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// Batch-local brush composer cache key. -/// -internal readonly struct WebGPUBrushComposerCacheKey : IEquatable -{ - private readonly Brush brush; - private readonly Rectangle brushBounds; - private readonly bool includeBrushBounds; - - public WebGPUBrushComposerCacheKey(Brush brush, in Rectangle brushBounds, bool includeBrushBounds) - { - this.brush = brush; - this.brushBounds = brushBounds; - this.includeBrushBounds = includeBrushBounds; - } - - public bool Equals(WebGPUBrushComposerCacheKey other) - { - if (!ReferenceEquals(this.brush, other.brush) || - this.includeBrushBounds != other.includeBrushBounds) - { - return false; - } - - return !this.includeBrushBounds || this.brushBounds.Equals(other.brushBounds); - } - - public override bool Equals(object? obj) => obj is WebGPUBrushComposerCacheKey other && this.Equals(other); - - public override int GetHashCode() - { - int brushHash = RuntimeHelpers.GetHashCode(this.brush); - return this.includeBrushBounds - ? HashCode.Combine(brushHash, this.brushBounds, this.includeBrushBounds) - : HashCode.Combine(brushHash, this.includeBrushBounds); - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs deleted file mode 100644 index aa05904e7..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUBrushComposerFactory.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// Creates brush composers for WebGPU composition commands. -/// -internal static class WebGPUBrushComposerFactory -{ - /// - /// Returns whether WebGPU can compose directly. - /// - public static bool IsSupportedBrush(Brush brush) - { - if (brush is SolidBrush) - { - return true; - } - - return brush is ImageBrush; - } - - /// - /// Creates a brush composer for the given prepared command. - /// - /// The brush composer. - public static IWebGPUBrushComposer Create( - WebGPUFlushContext flushContext, - in PreparedCompositionCommand command) - where TPixel : unmanaged, IPixel - { - if (command.Brush is SolidBrush solidBrush) - { - return new WebGPUSolidBrushComposer(solidBrush); - } - - if (command.Brush is ImageBrush imageBrush) - { - return WebGPUImageBrushComposer.Create(flushContext, imageBrush, command.BrushBounds); - } - - throw new InvalidOperationException($"Unexpected brush type '{command.Brush.GetType().FullName}'."); - } - - /// - /// Creates a cache key for reusing brush composers within one batch. - /// - public static WebGPUBrushComposerCacheKey CreateCacheKey(in PreparedCompositionCommand command) - { - bool includeBrushBounds = command.Brush is ImageBrush; - return new WebGPUBrushComposerCacheKey(command.Brush, command.BrushBounds, includeBrushBounds); - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs deleted file mode 100644 index 7d4013181..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUCompositeCommonParameters.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// Common per-command composition values shared by all brush composers. -/// -internal readonly struct WebGPUCompositeCommonParameters -{ - public readonly int SourceOffsetX; - - public readonly int SourceOffsetY; - - public readonly int DestinationX; - - public readonly int DestinationY; - - public readonly int DestinationWidth; - - public readonly int DestinationHeight; - - public readonly int DestinationBufferWidth; - - public readonly int DestinationBufferHeight; - - public readonly int DestinationBufferOriginX; - - public readonly int DestinationBufferOriginY; - - public readonly float BlendPercentage; - - public readonly int ColorBlendingMode; - - public readonly int AlphaCompositionMode; - - public WebGPUCompositeCommonParameters( - int sourceOffsetX, - int sourceOffsetY, - int destinationX, - int destinationY, - int destinationWidth, - int destinationHeight, - int destinationBufferWidth, - int destinationBufferHeight, - int destinationBufferOriginX, - int destinationBufferOriginY, - float blendPercentage, - int colorBlendingMode, - int alphaCompositionMode) - { - this.SourceOffsetX = sourceOffsetX; - this.SourceOffsetY = sourceOffsetY; - this.DestinationX = destinationX; - this.DestinationY = destinationY; - this.DestinationWidth = destinationWidth; - this.DestinationHeight = destinationHeight; - this.DestinationBufferWidth = destinationBufferWidth; - this.DestinationBufferHeight = destinationBufferHeight; - this.DestinationBufferOriginX = destinationBufferOriginX; - this.DestinationBufferOriginY = destinationBufferOriginY; - this.BlendPercentage = blendPercentage; - this.ColorBlendingMode = colorBlendingMode; - this.AlphaCompositionMode = alphaCompositionMode; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs deleted file mode 100644 index 57ebefda8..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUImageBrushComposer{TPixel}.cs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Silk.NET.WebGPU; -using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// GPU brush composer for image brushes. -/// -/// The pixel type used by the target composition surface. -internal sealed unsafe class WebGPUImageBrushComposer : IWebGPUBrushComposer - where TPixel : unmanaged, IPixel -{ - private const string PipelineKey = "image-brush"; - private readonly TextureView* sourceTextureView; - private readonly Rectangle sourceRegion; - private readonly int imageBrushOriginX; - private readonly int imageBrushOriginY; - private BindGroup* cachedBindGroup; - private nint cachedCoverageView; - private nint cachedDestinationBuffer; - private nuint cachedInstanceBytes; - private BindGroupLayout* bindGroupLayout; - private ComputePipeline* computePipeline; - - private WebGPUImageBrushComposer( - TextureView* sourceTextureView, - in Rectangle sourceRegion, - int imageBrushOriginX, - int imageBrushOriginY) - { - this.sourceTextureView = sourceTextureView; - this.sourceRegion = sourceRegion; - this.imageBrushOriginX = imageBrushOriginX; - this.imageBrushOriginY = imageBrushOriginY; - } - - /// - public nuint InstanceDataSizeInBytes => (nuint)Unsafe.SizeOf(); - - /// - public bool TryGetOrCreatePipeline( - WebGPUFlushContext flushContext, - out ComputePipeline* pipeline, - out string? error) - { - if (this.computePipeline is not null) - { - pipeline = this.computePipeline; - error = null; - return true; - } - - bool success = flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - PipelineKey, - ImageBrushCompositeComputeShader.Code, - TryCreateBindGroupLayout, - out this.bindGroupLayout, - out pipeline, - out error); - - if (success) - { - this.computePipeline = pipeline; - } - - return success; - } - - /// - /// Creates a composer for one image brush command. - /// - public static WebGPUImageBrushComposer Create( - WebGPUFlushContext flushContext, - ImageBrush imageBrush, - Rectangle brushBounds) - { - Guard.NotNull(flushContext, nameof(flushContext)); - Guard.NotNull(imageBrush, nameof(imageBrush)); - - // Invariant: image brushes have already been normalized for the target TPixel path. - Image sourceImage = (Image)imageBrush.SourceImage; - - Rectangle sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); - if (!flushContext.TryGetOrCreateSourceTextureView(sourceImage, out TextureView* sourceView)) - { - throw new InvalidOperationException("Failed to acquire source texture view for image brush composition."); - } - - int imageBrushOriginX = checked(brushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); - int imageBrushOriginY = checked(brushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); - return new WebGPUImageBrushComposer(sourceView, in sourceRegion, imageBrushOriginX, imageBrushOriginY); - } - - /// - public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination) - { - ImageBrushInstanceData data = new() - { - SourceOffsetX = common.SourceOffsetX, - SourceOffsetY = common.SourceOffsetY, - DestinationX = common.DestinationX, - DestinationY = common.DestinationY, - DestinationWidth = common.DestinationWidth, - DestinationHeight = common.DestinationHeight, - DestinationBufferWidth = common.DestinationBufferWidth, - DestinationBufferHeight = common.DestinationBufferHeight, - BlendPercentage = common.BlendPercentage, - ColorBlendingMode = common.ColorBlendingMode, - AlphaCompositionMode = common.AlphaCompositionMode, - CommonPadding0 = 0, - ImageRegionX = this.sourceRegion.X, - ImageRegionY = this.sourceRegion.Y, - ImageRegionWidth = this.sourceRegion.Width, - ImageRegionHeight = this.sourceRegion.Height, - ImageBrushOriginX = this.imageBrushOriginX - common.DestinationBufferOriginX, - ImageBrushOriginY = this.imageBrushOriginY - common.DestinationBufferOriginY, - Padding0 = 0, - Padding1 = 0 - }; - - MemoryMarshal.Write(destination, in data); - } - - /// - public BindGroup* CreateBindGroup( - WebGPUFlushContext flushContext, - TextureView* coverageView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - nuint instanceOffset, - nuint instanceBytes) - { - _ = instanceOffset; - nint coverageKey = (nint)coverageView; - nint destinationBufferKey = (nint)destinationPixelsBuffer; - if (this.cachedBindGroup is not null && - this.cachedCoverageView == coverageKey && - this.cachedDestinationBuffer == destinationBufferKey && - this.cachedInstanceBytes == instanceBytes) - { - return this.cachedBindGroup; - } - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[4]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageView - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = flushContext.InstanceBuffer, - Offset = 0, - Size = instanceBytes - }; - bindGroupEntries[2] = new BindGroupEntry - { - Binding = 2, - TextureView = this.sourceTextureView - }; - bindGroupEntries[3] = new BindGroupEntry - { - Binding = 3, - Buffer = destinationPixelsBuffer, - Size = destinationPixelsByteSize - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = this.bindGroupLayout, - EntryCount = 4, - Entries = bindGroupEntries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - throw new InvalidOperationException("Failed to create image brush bind group."); - } - - flushContext.TrackBindGroup(bindGroup); - this.cachedBindGroup = bindGroup; - this.cachedCoverageView = coverageKey; - this.cachedDestinationBuffer = destinationBufferKey; - this.cachedInstanceBytes = instanceBytes; - return bindGroup; - } - - private static bool TryCreateBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[4]; - layoutEntries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - layoutEntries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = true, - MinBindingSize = 0 - } - }; - layoutEntries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - layoutEntries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - - BindGroupLayoutDescriptor layoutDescriptor = new() - { - EntryCount = 4, - Entries = layoutEntries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in layoutDescriptor); - if (layout is null) - { - error = "Failed to create image composite bind group layout."; - return false; - } - - error = null; - return true; - } - - [StructLayout(LayoutKind.Sequential)] - private struct ImageBrushInstanceData - { - public int SourceOffsetX; - public int SourceOffsetY; - public int DestinationX; - public int DestinationY; - public int DestinationWidth; - public int DestinationHeight; - public int DestinationBufferWidth; - public int DestinationBufferHeight; - public float BlendPercentage; - public int ColorBlendingMode; - public int AlphaCompositionMode; - public int CommonPadding0; - public int ImageRegionX; - public int ImageRegionY; - public int ImageRegionWidth; - public int ImageRegionHeight; - public int ImageBrushOriginX; - public int ImageBrushOriginY; - public int Padding0; - public int Padding1; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs b/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs deleted file mode 100644 index c760bafbb..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Brushes/WebGPUSolidBrushComposer.cs +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Silk.NET.WebGPU; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; - -/// -/// GPU brush composer for solid-color brushes. -/// -internal sealed unsafe class WebGPUSolidBrushComposer : IWebGPUBrushComposer -{ - private const string PipelineKey = "solid-brush"; - private readonly Vector4 color; - private BindGroup* cachedBindGroup; - private nint cachedCoverageView; - private nint cachedDestinationBuffer; - private nuint cachedInstanceBytes; - private BindGroupLayout* bindGroupLayout; - private ComputePipeline* computePipeline; - - public WebGPUSolidBrushComposer(SolidBrush brush) - { - Guard.NotNull(brush, nameof(brush)); - this.color = brush.Color.ToScaledVector4(); - } - - /// - public nuint InstanceDataSizeInBytes => (nuint)Unsafe.SizeOf(); - - /// - public bool TryGetOrCreatePipeline( - WebGPUFlushContext flushContext, - out ComputePipeline* pipeline, - out string? error) - { - if (this.computePipeline is not null) - { - pipeline = this.computePipeline; - error = null; - return true; - } - - bool success = flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - PipelineKey, - SolidBrushCompositeComputeShader.Code, - TryCreateBindGroupLayout, - out this.bindGroupLayout, - out pipeline, - out error); - - if (success) - { - this.computePipeline = pipeline; - } - - return success; - } - - /// - public void WriteInstanceData(in WebGPUCompositeCommonParameters common, Span destination) - { - SolidBrushInstanceData data = new() - { - SourceOffsetX = common.SourceOffsetX, - SourceOffsetY = common.SourceOffsetY, - DestinationX = common.DestinationX, - DestinationY = common.DestinationY, - DestinationWidth = common.DestinationWidth, - DestinationHeight = common.DestinationHeight, - DestinationBufferWidth = common.DestinationBufferWidth, - DestinationBufferHeight = common.DestinationBufferHeight, - BlendPercentage = common.BlendPercentage, - ColorBlendingMode = common.ColorBlendingMode, - AlphaCompositionMode = common.AlphaCompositionMode, - Padding0 = 0, - SolidBrushColor = this.color - }; - - MemoryMarshal.Write(destination, in data); - } - - /// - public BindGroup* CreateBindGroup( - WebGPUFlushContext flushContext, - TextureView* coverageView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - nuint instanceOffset, - nuint instanceBytes) - { - _ = instanceOffset; - nint coverageKey = (nint)coverageView; - nint destinationBufferKey = (nint)destinationPixelsBuffer; - if (this.cachedBindGroup is not null && - this.cachedCoverageView == coverageKey && - this.cachedDestinationBuffer == destinationBufferKey && - this.cachedInstanceBytes == instanceBytes) - { - return this.cachedBindGroup; - } - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[3]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageView - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = flushContext.InstanceBuffer, - Offset = 0, - Size = instanceBytes - }; - bindGroupEntries[2] = new BindGroupEntry - { - Binding = 2, - Buffer = destinationPixelsBuffer, - Size = destinationPixelsByteSize - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = this.bindGroupLayout, - EntryCount = 3, - Entries = bindGroupEntries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - throw new InvalidOperationException("Failed to create solid brush bind group."); - } - - flushContext.TrackBindGroup(bindGroup); - this.cachedBindGroup = bindGroup; - this.cachedCoverageView = coverageKey; - this.cachedDestinationBuffer = destinationBufferKey; - this.cachedInstanceBytes = instanceBytes; - return bindGroup; - } - - private static bool TryCreateBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* layoutEntries = stackalloc BindGroupLayoutEntry[3]; - layoutEntries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - layoutEntries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = true, - MinBindingSize = 0 - } - }; - layoutEntries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - - BindGroupLayoutDescriptor layoutDescriptor = new() - { - EntryCount = 3, - Entries = layoutEntries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in layoutDescriptor); - if (layout is null) - { - error = "Failed to create solid composite bind group layout."; - return false; - } - - error = null; - return true; - } - - [StructLayout(LayoutKind.Sequential)] - private struct SolidBrushInstanceData - { - public int SourceOffsetX; - public int SourceOffsetY; - public int DestinationX; - public int DestinationY; - public int DestinationWidth; - public int DestinationHeight; - public int DestinationBufferWidth; - public int DestinationBufferHeight; - public float BlendPercentage; - public int ColorBlendingMode; - public int AlphaCompositionMode; - public int Padding0; - public Vector4 SolidBrushColor; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs deleted file mode 100644 index 5518d502d..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/SolidBrushCompositeComputeShader.cs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal static class SolidBrushCompositeComputeShader -{ - // Compile-time constant backed by static PE data (no heap allocation). - public static ReadOnlySpan Code => - """ - struct SolidBrushCompositeData { - source_offset_x: i32, - source_offset_y: i32, - destination_x: i32, - destination_y: i32, - destination_width: i32, - destination_height: i32, - destination_buffer_width: i32, - destination_buffer_height: i32, - blend_percentage: f32, - color_blending_mode: i32, - alpha_composition_mode: i32, - _common_pad0: i32, - solid_brush_color: vec4, - }; - - @group(0) @binding(0) - var coverage: texture_2d; - - @group(0) @binding(1) - var instance: SolidBrushCompositeData; - - @group(0) @binding(2) - var destination_pixels: array>; - - fn overlay_value(backdrop: f32, source: f32) -> f32 { - if (backdrop <= 0.5) { - return 2.0 * backdrop * source; - } - - return 1.0 - (2.0 * (1.0 - source) * (1.0 - backdrop)); - } - - fn blend_color(backdrop: vec3, source: vec3, color_mode: i32) -> vec3 { - switch color_mode { - case 0 { - return source; - } - - case 1 { - return backdrop * source; - } - - case 2 { - return min(vec3(1.0), backdrop + source); - } - - case 3 { - return max(vec3(0.0), backdrop - source); - } - - case 4 { - return vec3(1.0) - ((vec3(1.0) - backdrop) * (vec3(1.0) - source)); - } - - case 5 { - return min(backdrop, source); - } - - case 6 { - return max(backdrop, source); - } - - case 7 { - return vec3( - overlay_value(backdrop.r, source.r), - overlay_value(backdrop.g, source.g), - overlay_value(backdrop.b, source.b)); - } - - case 8 { - return vec3( - overlay_value(source.r, backdrop.r), - overlay_value(source.g, backdrop.g), - overlay_value(source.b, backdrop.b)); - } - - default { - return source; - } - } - } - - fn unpremultiply(premultiplied_rgb: vec3, alpha: f32) -> vec4 { - let clamped_alpha = clamp(alpha, 0.0, 1.0); - if (clamped_alpha <= 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let color = clamp( - premultiplied_rgb / clamped_alpha, - vec3(0.0), - vec3(1.0)); - return vec4(color, clamped_alpha); - } - - fn compose_over(destination: vec4, source: vec4, blend: vec3) -> vec4 { - let source_weight = source.a; - let destination_weight = destination.a; - let blend_weight = source_weight * destination_weight; - let destination_only_weight = destination_weight - blend_weight; - let source_only_weight = source_weight - blend_weight; - let alpha = destination_only_weight + source_weight; - let premultiplied_color = - (destination.rgb * destination_only_weight) + - (source.rgb * source_only_weight) + - (blend * blend_weight); - return unpremultiply(premultiplied_color, alpha); - } - - fn compose_atop(destination: vec4, source: vec4, blend: vec3) -> vec4 { - let source_weight = source.a; - let destination_weight = destination.a; - let blend_weight = source_weight * destination_weight; - let destination_only_weight = destination_weight - blend_weight; - let premultiplied_color = - (destination.rgb * destination_only_weight) + - (blend * blend_weight); - return unpremultiply(premultiplied_color, destination_weight); - } - - fn compose_in(destination: vec4, source: vec4) -> vec4 { - let alpha = destination.a * source.a; - return unpremultiply(source.rgb * alpha, alpha); - } - - fn compose_out(destination: vec4, source: vec4) -> vec4 { - let alpha = (1.0 - destination.a) * source.a; - return unpremultiply(source.rgb * alpha, alpha); - } - - fn compose_xor(destination: vec4, source: vec4) -> vec4 { - let source_weight = 1.0 - destination.a; - let destination_weight = 1.0 - source.a; - let alpha = (source.a * source_weight) + (destination.a * destination_weight); - let premultiplied_color = - (source.a * source.rgb * source_weight) + - (destination.a * destination.rgb * destination_weight); - return unpremultiply(premultiplied_color, alpha); - } - - fn compose_pixel( - destination: vec4, - source: vec4, - blend_percentage: f32, - color_mode: i32, - alpha_mode: i32) -> vec4 { - let source_alpha = clamp(source.a * blend_percentage, 0.0, 1.0); - let source_color = clamp(source.rgb, vec3(0.0), vec3(1.0)); - let source_with_opacity = vec4(source_color, source_alpha); - let destination_color = clamp(destination.rgb, vec3(0.0), vec3(1.0)); - let destination_alpha = clamp(destination.a, 0.0, 1.0); - let destination_pixel = vec4(destination_color, destination_alpha); - - switch alpha_mode { - case 0 { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_over(destination_pixel, source_with_opacity, blend); - } - - case 1 { - return source_with_opacity; - } - - case 2 { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_atop(destination_pixel, source_with_opacity, blend); - } - - case 3 { - return compose_in(destination_pixel, source_with_opacity); - } - - case 4 { - return compose_out(destination_pixel, source_with_opacity); - } - - case 5 { - return destination_pixel; - } - - case 6 { - let blend = blend_color(source_color, destination_color, color_mode); - return compose_atop(source_with_opacity, destination_pixel, blend); - } - - case 7 { - let blend = blend_color(source_color, destination_color, color_mode); - return compose_over(source_with_opacity, destination_pixel, blend); - } - - case 8 { - return compose_in(source_with_opacity, destination_pixel); - } - - case 9 { - return compose_out(source_with_opacity, destination_pixel); - } - - case 10 { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - case 11 { - return compose_xor(destination_pixel, source_with_opacity); - } - - default { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_over(destination_pixel, source_with_opacity, blend); - } - } - } - - @compute @workgroup_size(8, 8, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let params = instance; - let local_x = i32(global_id.x); - let local_y = i32(global_id.y); - if (local_x >= params.destination_width || local_y >= params.destination_height) { - return; - } - - let destination_pixel_x = params.destination_x + local_x; - let destination_pixel_y = params.destination_y + local_y; - if (destination_pixel_x < 0 || - destination_pixel_y < 0 || - destination_pixel_x >= params.destination_buffer_width || - destination_pixel_y >= params.destination_buffer_height) { - return; - } - - let coverage_source = vec2( - params.source_offset_x + local_x, - params.source_offset_y + local_y); - let coverage_value = textureLoad(coverage, coverage_source, 0).r; - let brush = params.solid_brush_color; - let source = vec4(brush.rgb, brush.a * coverage_value); - - let destination_index = (destination_pixel_y * params.destination_buffer_width) + destination_pixel_x; - let destination = destination_pixels[destination_index]; - destination_pixels[destination_index] = compose_pixel( - destination, - source, - params.blend_percentage, - params.color_blending_mode, - params.alpha_composition_mode); - } - """u8; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/TiledCompositeComputeShader.cs similarity index 65% rename from src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/TiledCompositeComputeShader.cs index d68d73943..e694e7892 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/ImageBrushCompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/TiledCompositeComputeShader.cs @@ -3,46 +3,73 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -internal static class ImageBrushCompositeComputeShader +internal static class TiledCompositeComputeShader { // Compile-time constant backed by static PE data (no heap allocation). public static ReadOnlySpan Code => """ - struct ImageBrushCompositeData { + struct CompositeCommand { source_offset_x: i32, source_offset_y: i32, destination_x: i32, destination_y: i32, destination_width: i32, destination_height: i32, - destination_buffer_width: i32, - destination_buffer_height: i32, blend_percentage: f32, color_blending_mode: i32, alpha_composition_mode: i32, - _common_pad0: i32, - image_region_x: i32, - image_region_y: i32, - image_region_width: i32, - image_region_height: i32, - image_brush_origin_x: i32, - image_brush_origin_y: i32, + brush_data_index: i32, _pad0: i32, _pad1: i32, }; + struct TileRange { + start_index: u32, + count: u32, + }; + + struct BrushData { + source_region_x: i32, + source_region_y: i32, + source_region_width: i32, + source_region_height: i32, + brush_origin_x: i32, + brush_origin_y: i32, + source_layer: i32, + _pad0: i32, + }; + + struct TiledCompositeParams { + destination_width: i32, + destination_height: i32, + tiles_x: i32, + tile_size: i32, + }; + @group(0) @binding(0) var coverage: texture_2d; @group(0) @binding(1) - var instance: ImageBrushCompositeData; + var commands: array; @group(0) @binding(2) - var source_image: texture_2d; + var tile_ranges: array; @group(0) @binding(3) + var tile_command_indices: array; + + @group(0) @binding(4) + var brushes: array; + + @group(0) @binding(5) + var source_layers: texture_2d_array; + + @group(0) @binding(6) var destination_pixels: array>; + @group(0) @binding(7) + var params: TiledCompositeParams; + fn overlay_value(backdrop: f32, source: f32) -> f32 { if (backdrop <= 0.5) { return 2.0 * backdrop * source; @@ -236,49 +263,82 @@ fn positive_mod(value: i32, divisor: i32) -> i32 { return ((value % divisor) + divisor) % divisor; } - fn sample_brush(params: ImageBrushCompositeData, destination_x: i32, destination_y: i32) -> vec4 { - if (params.image_region_width <= 0 || params.image_region_height <= 0) { + fn sample_brush(brush_data: BrushData, destination_x: i32, destination_y: i32) -> vec4 { + if (brush_data.source_region_width <= 0 || brush_data.source_region_height <= 0) { return vec4(0.0, 0.0, 0.0, 0.0); } - let source_x = positive_mod(destination_x - params.image_brush_origin_x, params.image_region_width) + params.image_region_x; - let source_y = positive_mod(destination_y - params.image_brush_origin_y, params.image_region_height) + params.image_region_y; - return textureLoad(source_image, vec2(source_x, source_y), 0); + let source_x = positive_mod( + destination_x - brush_data.brush_origin_x, + brush_data.source_region_width) + brush_data.source_region_x; + let source_y = positive_mod( + destination_y - brush_data.brush_origin_y, + brush_data.source_region_height) + brush_data.source_region_y; + return textureLoad(source_layers, vec2(source_x, source_y), brush_data.source_layer, 0); } @compute @workgroup_size(8, 8, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let params = instance; - let local_x = i32(global_id.x); - let local_y = i32(global_id.y); - if (local_x >= params.destination_width || local_y >= params.destination_height) { + fn cs_main( + @builtin(workgroup_id) workgroup_id: vec3, + @builtin(local_invocation_id) local_id: vec3) + { + let tile_x = i32(workgroup_id.x); + let tile_y = i32(workgroup_id.y); + if (tile_x < 0 || tile_x >= params.tiles_x || tile_y < 0) { return; } - let destination_pixel_x = params.destination_x + local_x; - let destination_pixel_y = params.destination_y + local_y; - if (destination_pixel_x < 0 || - destination_pixel_y < 0 || - destination_pixel_x >= params.destination_buffer_width || - destination_pixel_y >= params.destination_buffer_height) { + let pixel_x = tile_x * params.tile_size + i32(local_id.x); + let pixel_y = tile_y * params.tile_size + i32(local_id.y); + if (pixel_x < 0 || + pixel_y < 0 || + pixel_x >= params.destination_width || + pixel_y >= params.destination_height) + { return; } - let coverage_source = vec2( - params.source_offset_x + local_x, - params.source_offset_y + local_y); - let coverage_value = textureLoad(coverage, coverage_source, 0).r; - let brush = sample_brush(params, destination_pixel_x, destination_pixel_y); - let source = vec4(brush.rgb, brush.a * coverage_value); - - let destination_index = (destination_pixel_y * params.destination_buffer_width) + destination_pixel_x; - let destination = destination_pixels[destination_index]; - destination_pixels[destination_index] = compose_pixel( - destination, - source, - params.blend_percentage, - params.color_blending_mode, - params.alpha_composition_mode); + let destination_index = (pixel_y * params.destination_width) + pixel_x; + var destination = destination_pixels[destination_index]; + + let tile_index = (tile_y * params.tiles_x) + tile_x; + let tile_range = tile_ranges[tile_index]; + let tile_end = tile_range.start_index + tile_range.count; + var tile_cursor = tile_range.start_index; + loop { + if (tile_cursor >= tile_end) { + break; + } + + let command_index = tile_command_indices[tile_cursor]; + let command = commands[command_index]; + if (pixel_x >= command.destination_x && + pixel_y >= command.destination_y && + pixel_x < (command.destination_x + command.destination_width) && + pixel_y < (command.destination_y + command.destination_height)) + { + let local_x = pixel_x - command.destination_x; + let local_y = pixel_y - command.destination_y; + let coverage_source = vec2( + command.source_offset_x + local_x, + command.source_offset_y + local_y); + let coverage_value = textureLoad(coverage, coverage_source, 0).r; + if (coverage_value > 0.0) { + let brush = sample_brush(brushes[command.brush_data_index], pixel_x, pixel_y); + let source = vec4(brush.rgb, brush.a * coverage_value); + destination = compose_pixel( + destination, + source, + command.blend_percentage, + command.color_blending_mode, + command.alpha_composition_mode); + } + } + + tile_cursor = tile_cursor + 1u; + } + + destination_pixels[destination_index] = destination; } """u8; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs new file mode 100644 index 000000000..4d5fcfdb0 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs @@ -0,0 +1,823 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal sealed unsafe partial class WebGPUDrawingBackend +{ + private const int TiledCompositeTileSize = CompositeComputeWorkgroupSize; + private const string TiledCompositePipelineKey = "tiled-composite"; + + private bool TryCompositeBatchTiled( + WebGPUFlushContext flushContext, + TextureView* coverageView, + IReadOnlyList commands, + bool blitToTarget, + out string? error) + where TPixel : unmanaged, IPixel + { + error = null; + if (commands.Count == 0) + { + return true; + } + + Rectangle targetLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); + if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) + { + return true; + } + + if (!flushContext.EnsureCommandEncoder()) + { + error = "Failed to create WebGPU command encoder."; + return false; + } + + if (flushContext.TargetTexture is null || flushContext.TargetView is null || coverageView is null) + { + error = "WebGPU flush context does not expose required target/coverage resources."; + return false; + } + + WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; + nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; + if (destinationPixelsBuffer is null) + { + TextureView* sourceTextureView = flushContext.TargetView; + if (!flushContext.CanSampleTargetTexture) + { + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out Texture* sourceTexture, + out sourceTextureView, + out error)) + { + return false; + } + + CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); + } + + if (!TryCreateDestinationPixelsBuffer( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out destinationPixelsBuffer, + out destinationPixelsByteSize, + out error) || + !TryInitializeDestinationPixels( + flushContext, + sourceTextureView, + destinationPixelsBuffer, + targetLocalBounds.Width, + targetLocalBounds.Height, + destinationPixelsByteSize, + out error)) + { + return false; + } + + flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; + flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; + } + + if (!this.TryRunTiledCompositeComputePass( + flushContext, + coverageView, + destinationPixelsBuffer, + destinationPixelsByteSize, + commands, + targetLocalBounds.Width, + targetLocalBounds.Height, + out error)) + { + return false; + } + + this.TestingComputePathBatchCount++; + + if (blitToTarget && + !TryBlitDestinationPixelsToTarget( + flushContext, + destinationPixelsBuffer, + destinationPixelsByteSize, + targetLocalBounds, + out error)) + { + return false; + } + + return true; + } + + private bool TryRunTiledCompositeComputePass( + WebGPUFlushContext flushContext, + TextureView* coverageView, + WgpuBuffer* destinationPixelsBuffer, + nuint destinationPixelsByteSize, + IReadOnlyList commands, + int destinationWidth, + int destinationHeight, + out string? error) + where TPixel : unmanaged, IPixel + { + error = null; + int commandCount = commands.Count; + if (commandCount == 0) + { + return true; + } + + int tilesX = (destinationWidth + TiledCompositeTileSize - 1) / TiledCompositeTileSize; + int tilesY = (destinationHeight + TiledCompositeTileSize - 1) / TiledCompositeTileSize; + if (tilesX <= 0 || tilesY <= 0) + { + return true; + } + + int tileCount = checked(tilesX * tilesY); + int[] rentedTileCounts = ArrayPool.Shared.Rent(tileCount); + Array.Clear(rentedTileCounts, 0, tileCount); + Span tileCommandCounts = rentedTileCounts.AsSpan(0, tileCount); + + TiledCompositeCommandData[] commandData = new TiledCompositeCommandData[commandCount]; + List brushData = []; + List> sourceLayers = []; + Dictionary sourceImageLayers = new(ReferenceEqualityComparer.Instance); + Dictionary solidColorLayers = []; + + try + { + for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + { + PreparedCompositionCommand command = commands[commandIndex]; + Rectangle destinationRegion = command.DestinationRegion; + if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) + { + continue; + } + + int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); + int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); + int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); + int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); + for (int tileY = minTileY; tileY <= maxTileY; tileY++) + { + int rowStart = checked(tileY * tilesX); + for (int tileX = minTileX; tileX <= maxTileX; tileX++) + { + tileCommandCounts[rowStart + tileX]++; + } + } + + int sourceLayer; + Rectangle sourceRegion; + int brushOriginX; + int brushOriginY; + if (command.Brush is ImageBrush imageBrush) + { + Image sourceImage = (Image)imageBrush.SourceImage; + if (!sourceImageLayers.TryGetValue(sourceImage, out sourceLayer)) + { + sourceLayer = sourceLayers.Count; + sourceImageLayers.Add(sourceImage, sourceLayer); + sourceLayers.Add(TiledSourceLayer.CreateImage(sourceImage)); + } + + sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); + brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); + brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); + } + else if (command.Brush is SolidBrush solidBrush) + { + TPixel solidPixel = solidBrush.Color.ToPixel(); + if (!solidColorLayers.TryGetValue(solidPixel, out sourceLayer)) + { + sourceLayer = sourceLayers.Count; + solidColorLayers.Add(solidPixel, sourceLayer); + sourceLayers.Add(TiledSourceLayer.CreateSolid(solidPixel)); + } + + sourceRegion = new Rectangle(0, 0, 1, 1); + brushOriginX = 0; + brushOriginY = 0; + } + else + { + error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; + return false; + } + + int brushDataIndex = brushData.Count; + brushData.Add( + new TiledCompositeBrushData( + sourceRegion.X, + sourceRegion.Y, + sourceRegion.Width, + sourceRegion.Height, + brushOriginX, + brushOriginY, + sourceLayer)); + + GraphicsOptions options = command.GraphicsOptions; + commandData[commandIndex] = new TiledCompositeCommandData( + command.SourceOffset.X, + command.SourceOffset.Y, + destinationRegion.X, + destinationRegion.Y, + destinationRegion.Width, + destinationRegion.Height, + options.BlendPercentage, + (int)options.ColorBlendingMode, + (int)options.AlphaCompositionMode, + brushDataIndex); + } + + TiledCompositeTileRange[] tileRanges = new TiledCompositeTileRange[tileCount]; + int totalTileCommandRefs = 0; + for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + int count = tileCommandCounts[tileIndex]; + tileRanges[tileIndex] = new TiledCompositeTileRange((uint)totalTileCommandRefs, (uint)count); + tileCommandCounts[tileIndex] = totalTileCommandRefs; + totalTileCommandRefs = checked(totalTileCommandRefs + count); + } + + uint[] tileCommandIndices = new uint[Math.Max(totalTileCommandRefs, 1)]; + for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + { + Rectangle destinationRegion = commands[commandIndex].DestinationRegion; + if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) + { + continue; + } + + int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); + int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); + int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); + int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); + for (int tileY = minTileY; tileY <= maxTileY; tileY++) + { + int rowStart = checked(tileY * tilesX); + for (int tileX = minTileX; tileX <= maxTileX; tileX++) + { + int tileIndex = rowStart + tileX; + int writeIndex = tileCommandCounts[tileIndex]++; + tileCommandIndices[writeIndex] = (uint)commandIndex; + } + } + } + + if (!TryCreateSourceLayerTextureArray(flushContext, sourceLayers, out TextureView* sourceLayerView, out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + commandData.AsSpan(), + out WgpuBuffer* commandBuffer, + out nuint commandBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + tileRanges.AsSpan(), + out WgpuBuffer* tileRangeBuffer, + out nuint tileRangeBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + tileCommandIndices.AsSpan(), + out WgpuBuffer* tileCommandIndexBuffer, + out nuint tileCommandIndexBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + CollectionsMarshal.AsSpan(brushData), + out WgpuBuffer* brushDataBuffer, + out nuint brushDataBufferBytes, + out error)) + { + return false; + } + + TiledCompositeParameters parameters = new(destinationWidth, destinationHeight, tilesX, TiledCompositeTileSize); + if (!TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Uniform, + MemoryMarshal.CreateReadOnlySpan(ref parameters, 1), + out WgpuBuffer* parameterBuffer, + out nuint parameterBufferBytes, + out error)) + { + return false; + } + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + TiledCompositePipelineKey, + TiledCompositeComputeShader.Code, + TryCreateTiledCompositeBindGroupLayout, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + BindGroupEntry* entries = stackalloc BindGroupEntry[8]; + entries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageView + }; + entries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = commandBuffer, + Offset = 0, + Size = commandBufferBytes + }; + entries[2] = new BindGroupEntry + { + Binding = 2, + Buffer = tileRangeBuffer, + Offset = 0, + Size = tileRangeBufferBytes + }; + entries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = tileCommandIndexBuffer, + Offset = 0, + Size = tileCommandIndexBufferBytes + }; + entries[4] = new BindGroupEntry + { + Binding = 4, + Buffer = brushDataBuffer, + Offset = 0, + Size = brushDataBufferBytes + }; + entries[5] = new BindGroupEntry + { + Binding = 5, + TextureView = sourceLayerView + }; + entries[6] = new BindGroupEntry + { + Binding = 6, + Buffer = destinationPixelsBuffer, + Offset = 0, + Size = destinationPixelsByteSize + }; + entries[7] = new BindGroupEntry + { + Binding = 7, + Buffer = parameterBuffer, + Offset = 0, + Size = parameterBufferBytes + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 8, + Entries = entries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create tiled composite bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin tiled composite compute pass."; + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, (uint)tilesX, (uint)tilesY, 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + return true; + } + finally + { + ArrayPool.Shared.Return(rentedTileCounts); + } + } + + private static bool TryCreateSourceLayerTextureArray( + WebGPUFlushContext flushContext, + List> sourceLayers, + out TextureView* sourceLayerView, + out string? error) + where TPixel : unmanaged, IPixel + { + int layerCount = Math.Max(1, sourceLayers.Count); + int maxWidth = 1; + int maxHeight = 1; + for (int i = 0; i < sourceLayers.Count; i++) + { + TiledSourceLayer layer = sourceLayers[i]; + if (layer.Image is null) + { + continue; + } + + if (layer.Image.Width > maxWidth) + { + maxWidth = layer.Image.Width; + } + + if (layer.Image.Height > maxHeight) + { + maxHeight = layer.Image.Height; + } + } + + TextureDescriptor descriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)maxWidth, (uint)maxHeight, (uint)layerCount), + Format = flushContext.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); + if (texture is null) + { + sourceLayerView = null; + error = "Failed to create source-layer texture array."; + return false; + } + + TextureViewDescriptor viewDescriptor = new() + { + Format = flushContext.TextureFormat, + Dimension = TextureViewDimension.Dimension2DArray, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = (uint)layerCount, + Aspect = TextureAspect.All + }; + + sourceLayerView = flushContext.Api.TextureCreateView(texture, in viewDescriptor); + if (sourceLayerView is null) + { + flushContext.Api.TextureRelease(texture); + error = "Failed to create source-layer texture array view."; + return false; + } + + try + { + if (sourceLayers.Count == 0) + { + UploadSolidSourceLayer(flushContext, texture, default(TPixel), 0); + } + else + { + for (int i = 0; i < sourceLayers.Count; i++) + { + TiledSourceLayer layer = sourceLayers[i]; + if (layer.Image is not null) + { + Buffer2DRegion sourceRegion = new(layer.Image.Frames.RootFrame.PixelBuffer, layer.Image.Bounds); + WebGPUFlushContext.UploadTextureFromRegion( + flushContext.Api, + flushContext.Queue, + texture, + sourceRegion, + 0, + 0, + (uint)i); + } + else + { + UploadSolidSourceLayer(flushContext, texture, layer.SolidPixel, (uint)i); + } + } + } + } + catch (Exception ex) + { + flushContext.Api.TextureViewRelease(sourceLayerView); + flushContext.Api.TextureRelease(texture); + sourceLayerView = null; + error = $"Failed to upload source layers for tiled composition. {ex.Message}"; + return false; + } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(sourceLayerView); + error = null; + return true; + } + + private static void UploadSolidSourceLayer( + WebGPUFlushContext flushContext, + Texture* texture, + TPixel pixel, + uint layer) + where TPixel : unmanaged + { + ImageCopyTexture destination = new() + { + Texture = texture, + MipLevel = 0, + Origin = new Origin3D(0, 0, layer), + Aspect = TextureAspect.All + }; + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)Unsafe.SizeOf(), + RowsPerImage = 1 + }; + + Extent3D size = new(1, 1, 1); + TPixel copy = pixel; + flushContext.Api.QueueWriteTexture( + flushContext.Queue, + in destination, + ©, + (nuint)Unsafe.SizeOf(), + in layout, + in size); + } + + private static bool TryCreateAndUploadBuffer( + WebGPUFlushContext flushContext, + BufferUsage usage, + ReadOnlySpan sourceData, + out WgpuBuffer* buffer, + out nuint bufferSize, + out string? error) + where T : unmanaged + { + nuint elementSize = (nuint)Unsafe.SizeOf(); + nuint writeSize = checked((nuint)sourceData.Length * elementSize); + bufferSize = Math.Max(writeSize, Math.Max(elementSize, (nuint)16)); + + BufferDescriptor descriptor = new() + { + Usage = usage | BufferUsage.CopyDst, + Size = bufferSize + }; + + buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (buffer is null) + { + error = "Failed to create tiled composite buffer."; + return false; + } + + flushContext.TrackBuffer(buffer); + if (!sourceData.IsEmpty) + { + fixed (T* sourcePtr = sourceData) + { + flushContext.Api.QueueWriteBuffer(flushContext.Queue, buffer, 0, sourcePtr, writeSize); + } + } + + error = null; + return true; + } + + private static bool TryCreateTiledCompositeBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[5] = new BindGroupLayoutEntry + { + Binding = 5, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2DArray, + Multisampled = false + } + }; + entries[6] = new BindGroupLayoutEntry + { + Binding = 6, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[7] = new BindGroupLayoutEntry + { + Binding = 7, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 8, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create tiled composite bind-group layout."; + return false; + } + + error = null; + return true; + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct TiledCompositeCommandData( + int sourceOffsetX, + int sourceOffsetY, + int destinationX, + int destinationY, + int destinationWidth, + int destinationHeight, + float blendPercentage, + int colorBlendingMode, + int alphaCompositionMode, + int brushDataIndex) + { + public readonly int SourceOffsetX = sourceOffsetX; + public readonly int SourceOffsetY = sourceOffsetY; + public readonly int DestinationX = destinationX; + public readonly int DestinationY = destinationY; + public readonly int DestinationWidth = destinationWidth; + public readonly int DestinationHeight = destinationHeight; + public readonly float BlendPercentage = blendPercentage; + public readonly int ColorBlendingMode = colorBlendingMode; + public readonly int AlphaCompositionMode = alphaCompositionMode; + public readonly int BrushDataIndex = brushDataIndex; + public readonly int Padding0 = 0; + public readonly int Padding1 = 0; + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct TiledCompositeTileRange(uint startIndex, uint count) + { + public readonly uint StartIndex = startIndex; + public readonly uint Count = count; + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct TiledCompositeBrushData( + int sourceRegionX, + int sourceRegionY, + int sourceRegionWidth, + int sourceRegionHeight, + int brushOriginX, + int brushOriginY, + int sourceLayer) + { + public readonly int SourceRegionX = sourceRegionX; + public readonly int SourceRegionY = sourceRegionY; + public readonly int SourceRegionWidth = sourceRegionWidth; + public readonly int SourceRegionHeight = sourceRegionHeight; + public readonly int BrushOriginX = brushOriginX; + public readonly int BrushOriginY = brushOriginY; + public readonly int SourceLayer = sourceLayer; + public readonly int Padding0 = 0; + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct TiledCompositeParameters( + int destinationWidth, + int destinationHeight, + int tilesX, + int tileSize) + { + public readonly int DestinationWidth = destinationWidth; + public readonly int DestinationHeight = destinationHeight; + public readonly int TilesX = tilesX; + public readonly int TileSize = tileSize; + } + + private readonly struct TiledSourceLayer + where TPixel : unmanaged, IPixel + { + public TiledSourceLayer(Image image) + { + this.Image = image; + this.SolidPixel = default; + } + + public TiledSourceLayer(TPixel solidPixel) + { + this.Image = null; + this.SolidPixel = solidPixel; + } + + public Image? Image { get; } + + public TPixel SolidPixel { get; } + + public static TiledSourceLayer CreateImage(Image image) => new(image); + + public static TiledSourceLayer CreateSolid(TPixel solidPixel) => new(solidPixel); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 11587f069..9b0cfabc2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1,13 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; -using SixLabors.ImageSharp.Drawing.Processing.Backends.Brushes; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -23,7 +23,6 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private const uint CompositeVertexCount = 6; private const int CompositeComputeWorkgroupSize = 8; private const int CompositeDestinationPixelStride = 16; - private const nuint CompositeInstanceBufferSize = 256 * 1024; private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; @@ -89,6 +88,12 @@ public WebGPUDrawingBackend() ///
internal int TestingLiveCoverageCount { get; private set; } + /// + /// Gets the testing-only diagnostic counter for composition batches that used + /// the compute composition path. + /// + internal int TestingComputePathBatchCount { get; private set; } + internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) { this.ThrowIfDisposed(); @@ -131,7 +136,7 @@ public bool IsCompositionBrushSupported(Brush brush) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); - return WebGPUBrushComposerFactory.IsSupportedBrush(brush); + return IsSupportedCompositionBrush(brush); } /// @@ -275,7 +280,7 @@ private static bool AreAllCompositionBrushesSupported(IReadOnlyList brush is SolidBrush or ImageBrush; + private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, @@ -338,191 +346,12 @@ private bool TryCompositeBatch( out string? error) where TPixel : unmanaged, IPixel { - error = null; - int commandCount = commands.Count; - if (commandCount == 0) - { - return true; - } - - Rectangle targetLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); - if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) - { - return true; - } - - IWebGPUBrushComposer[] composers = new IWebGPUBrushComposer[commandCount]; - bool hasPreviousComposer = false; - WebGPUBrushComposerCacheKey previousComposerKey = default; - IWebGPUBrushComposer? previousComposer = null; - for (int i = 0; i < composers.Length; i++) - { - PreparedCompositionCommand command = commands[i]; - WebGPUBrushComposerCacheKey cacheKey = WebGPUBrushComposerFactory.CreateCacheKey(command); - IWebGPUBrushComposer? composer; - if (hasPreviousComposer && cacheKey.Equals(previousComposerKey)) - { - composer = previousComposer!; - } - else - { - composer = WebGPUBrushComposerFactory.Create(flushContext, command); - } - - composers[i] = composer!; - previousComposerKey = cacheKey; - previousComposer = composer!; - hasPreviousComposer = true; - } - - nuint totalInstanceBytes = 0; - for (int i = 0; i < composers.Length; i++) - { - nuint instanceBytes = composers[i].InstanceDataSizeInBytes; - totalInstanceBytes = checked(totalInstanceBytes + AlignToStorageBufferOffset(instanceBytes)); - } - - nuint instanceOffset = flushContext.InstanceBufferWriteOffset; - nuint requiredCapacity = checked(instanceOffset + totalInstanceBytes); - - // If the buffer exists but cannot fit at the current offset, flush pending - // draws and reset so the next batch starts at offset 0. - if (flushContext.InstanceBuffer is not null && - flushContext.InstanceBufferCapacity < requiredCapacity && - instanceOffset > 0) - { - if (!TrySubmitBatch(flushContext)) - { - return false; - } - - instanceOffset = 0; - requiredCapacity = totalInstanceBytes; - } - - if (!flushContext.EnsureInstanceBufferCapacity(requiredCapacity, Math.Max(requiredCapacity, CompositeInstanceBufferSize)) || - !flushContext.EnsureCommandEncoder()) - { - error = "Failed to allocate WebGPU composition buffers."; - return false; - } - - if (flushContext.TargetTexture is null || flushContext.TargetView is null) - { - error = "WebGPU flush context does not expose a target texture/view."; - return false; - } - - WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; - nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; - if (destinationPixelsBuffer is null) - { - // Initialize the destination buffer once per flush from the current target texture. - TextureView* sourceTextureView = flushContext.TargetView; - if (!flushContext.CanSampleTargetTexture) - { - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out Texture* sourceTexture, - out sourceTextureView, - out error)) - { - return false; - } - - CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); - } - - if (!TryCreateDestinationPixelsBuffer( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out destinationPixelsBuffer, - out destinationPixelsByteSize, - out error) || - !TryInitializeDestinationPixels( - flushContext, - sourceTextureView, - destinationPixelsBuffer, - targetLocalBounds.Width, - targetLocalBounds.Height, - destinationPixelsByteSize, - out error)) - { - return false; - } - - flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; - flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; - } - - Span instanceScratch = flushContext.GetCompositionInstanceScratchBuffer(checked((int)totalInstanceBytes)); - nuint localInstanceOffset = 0; - int destinationBufferWidth = targetLocalBounds.Width; - int destinationBufferHeight = targetLocalBounds.Height; - for (int i = 0; i < composers.Length; i++) - { - IWebGPUBrushComposer composer = composers[i]; - PreparedCompositionCommand command = commands[i]; - nuint instanceBytes = composer.InstanceDataSizeInBytes; - int instanceBytesInt = checked((int)instanceBytes); - int destinationX = command.DestinationRegion.X - flushContext.TargetBounds.X; - int destinationY = command.DestinationRegion.Y - flushContext.TargetBounds.Y; - WebGPUCompositeCommonParameters common = new( - command.SourceOffset.X, - command.SourceOffset.Y, - destinationX, - destinationY, - command.DestinationRegion.Width, - command.DestinationRegion.Height, - destinationBufferWidth, - destinationBufferHeight, - 0, - 0, - command.GraphicsOptions.BlendPercentage, - (int)command.GraphicsOptions.ColorBlendingMode, - (int)command.GraphicsOptions.AlphaCompositionMode); - - Span payload = instanceScratch.Slice(checked((int)localInstanceOffset), instanceBytesInt); - composer.WriteInstanceData(in common, payload); - localInstanceOffset = checked(localInstanceOffset + AlignToStorageBufferOffset(instanceBytes)); - } - - fixed (byte* payloadPtr = instanceScratch) - { - // Upload all instance payloads in one call to minimize queue write overhead. - flushContext.Api.QueueWriteBuffer(flushContext.Queue, flushContext.InstanceBuffer, instanceOffset, payloadPtr, totalInstanceBytes); - } - - if (!TryRunCompositeCommandComputePass( - flushContext, - coverageEntry.GPUCoverageView, - destinationPixelsBuffer, - destinationPixelsByteSize, - commands, - composers, - instanceOffset, - out nuint finalCommandOffset, - out error)) - { - return false; - } - - if (blitToTarget && - !TryBlitDestinationPixelsToTarget( - flushContext, - destinationPixelsBuffer, - destinationPixelsByteSize, - targetLocalBounds, - out error)) - { - return false; - } - - flushContext.AdvanceInstanceBufferOffset(finalCommandOffset); - return true; + return this.TryCompositeBatchTiled( + flushContext, + coverageEntry.GPUCoverageView, + commands, + blitToTarget, + out error); } private static bool TryCreateDestinationPixelsBuffer( @@ -627,87 +456,6 @@ private static bool TryInitializeDestinationPixels( return true; } - private static bool TryRunCompositeCommandComputePass( - WebGPUFlushContext flushContext, - TextureView* coverageView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - IReadOnlyList commands, - IWebGPUBrushComposer[] composers, - nuint instanceOffset, - out nuint finalCommandOffset, - out string? error) - { - finalCommandOffset = instanceOffset; - error = null; - ComputePassDescriptor passDescriptor = default; - ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); - if (passEncoder is null) - { - error = "Failed to begin WebGPU composition compute pass."; - return false; - } - - try - { - nuint commandOffset = instanceOffset; - IWebGPUBrushComposer? previousComposer = null; - ComputePipeline* previousComposerPipeline = null; - ComputePipeline* currentBoundPipeline = null; - for (int i = 0; i < composers.Length; i++) - { - IWebGPUBrushComposer composer = composers[i]; - PreparedCompositionCommand command = commands[i]; - nuint instanceBytes = composer.InstanceDataSizeInBytes; - ComputePipeline* pipeline; - if (ReferenceEquals(composer, previousComposer)) - { - pipeline = previousComposerPipeline; - } - else if (!composer.TryGetOrCreatePipeline(flushContext, out pipeline, out string? pipelineError)) - { - error = pipelineError ?? "Failed to create composite compute pipeline."; - return false; - } - - BindGroup* bindGroup = composer.CreateBindGroup( - flushContext, - coverageView, - destinationPixelsBuffer, - destinationPixelsByteSize, - commandOffset, - instanceBytes); - - if (pipeline != currentBoundPipeline) - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - currentBoundPipeline = pipeline; - } - - uint dynamicOffset = checked((uint)commandOffset); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 1, &dynamicOffset); - uint dispatchX = DivideRoundUp(command.DestinationRegion.Width, CompositeComputeWorkgroupSize); - uint dispatchY = DivideRoundUp(command.DestinationRegion.Height, CompositeComputeWorkgroupSize); - if (dispatchX > 0 && dispatchY > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, dispatchX, dispatchY, 1); - } - - commandOffset = checked(commandOffset + AlignToStorageBufferOffset(instanceBytes)); - previousComposer = composer; - previousComposerPipeline = pipeline; - } - - finalCommandOffset = commandOffset; - return true; - } - finally - { - flushContext.Api.ComputePassEncoderEnd(passEncoder); - flushContext.Api.ComputePassEncoderRelease(passEncoder); - } - } - private static bool TryBlitDestinationPixelsToTarget( WebGPUFlushContext flushContext, WgpuBuffer* destinationPixelsBuffer, @@ -720,6 +468,7 @@ private static bool TryBlitDestinationPixelsToTarget( CompositeDestinationBlitShader.Code, TryCreateDestinationBlitBindGroupLayout, flushContext.TextureFormat, + CompositePipelineBlendMode.None, out BindGroupLayout* bindGroupLayout, out RenderPipeline* pipeline, out error)) @@ -1196,10 +945,6 @@ public void Dispose() private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static nuint AlignToStorageBufferOffset(nuint value) - => (value + 255) & ~(nuint)255; - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) where T : struct diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index e2344d8a9..a5f9e67b5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -14,6 +14,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +internal enum CompositePipelineBlendMode +{ + None = 0 +} + /// /// Per-flush WebGPU execution context created from a single frame target. /// @@ -942,13 +947,24 @@ internal static void UploadTextureFromRegion( Texture* destinationTexture, Buffer2DRegion sourceRegion) where TPixel : unmanaged + => UploadTextureFromRegion(api, queue, destinationTexture, sourceRegion, 0, 0, 0); + + internal static void UploadTextureFromRegion( + WebGPU api, + Queue* queue, + Texture* destinationTexture, + Buffer2DRegion sourceRegion, + uint destinationX, + uint destinationY, + uint destinationLayer) + where TPixel : unmanaged { int pixelSizeInBytes = Unsafe.SizeOf(); ImageCopyTexture destination = new() { Texture = destinationTexture, MipLevel = 0, - Origin = new Origin3D(0, 0, 0), + Origin = new Origin3D(destinationX, destinationY, destinationLayer), Aspect = TextureAspect.All }; @@ -1116,6 +1132,7 @@ public bool TryGetOrCreateCompositePipeline( ReadOnlySpan shaderCode, WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, TextureFormat textureFormat, + CompositePipelineBlendMode blendMode, out BindGroupLayout* bindGroupLayout, out RenderPipeline* pipeline, out string? error) @@ -1168,7 +1185,8 @@ infrastructure.PipelineLayout is null || } bindGroupLayout = infrastructure.BindGroupLayout; - if (infrastructure.Pipelines.TryGetValue(textureFormat, out nint cachedPipelineHandle) && cachedPipelineHandle != 0) + (TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode) variantKey = (textureFormat, blendMode); + if (infrastructure.Pipelines.TryGetValue(variantKey, out nint cachedPipelineHandle) && cachedPipelineHandle != 0) { pipeline = (RenderPipeline*)cachedPipelineHandle; error = null; @@ -1178,14 +1196,15 @@ infrastructure.PipelineLayout is null || RenderPipeline* createdPipeline = this.CreateCompositePipeline( infrastructure.PipelineLayout, infrastructure.ShaderModule, - textureFormat); + textureFormat, + blendMode); if (createdPipeline is null) { error = $"Failed to create composite pipeline '{pipelineKey}' for format '{textureFormat}'."; return false; } - infrastructure.Pipelines[textureFormat] = (nint)createdPipeline; + infrastructure.Pipelines[variantKey] = (nint)createdPipeline; pipeline = createdPipeline; error = null; return true; @@ -1355,7 +1374,8 @@ private bool TryCreateCompositeInfrastructure( private RenderPipeline* CreateCompositePipeline( PipelineLayout* pipelineLayout, ShaderModule* shaderModule, - TextureFormat textureFormat) + TextureFormat textureFormat, + CompositePipelineBlendMode blendMode) { ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; @@ -1368,7 +1388,8 @@ private bool TryCreateCompositeInfrastructure( shaderModule, vertexEntryPointPtr, fragmentEntryPointPtr, - textureFormat); + textureFormat, + blendMode); } } } @@ -1378,8 +1399,10 @@ private bool TryCreateCompositeInfrastructure( ShaderModule* shaderModule, byte* vertexEntryPointPtr, byte* fragmentEntryPointPtr, - TextureFormat textureFormat) + TextureFormat textureFormat, + CompositePipelineBlendMode blendMode) { + _ = blendMode; VertexState vertexState = new() { Module = shaderModule, @@ -1549,7 +1572,7 @@ private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) private sealed class CompositePipelineInfrastructure { - public Dictionary Pipelines { get; } = []; + public Dictionary<(TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; public BindGroupLayout* BindGroupLayout { get; set; } diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs index 299ac3338..c6e022560 100644 --- a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs +++ b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs @@ -26,7 +26,9 @@ public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Co return false; } - if (options.AlphaCompositionMode is not PixelAlphaCompositionMode.SrcOver and not PixelAlphaCompositionMode.Src) + // Only the first two alpha composition enum values can fully replace backdrop + // for an opaque source at full blend amount. + if ((uint)options.AlphaCompositionMode > 1U) { return false; } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index b47b7c7ff..444ee17f5 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -130,7 +130,10 @@ public void FlushCompositions() definitionCommand.Path, definitionCommand.RasterizerOptions); - batches.Add(new CompositionBatch(definition, preparedCommands)); + batches.Add( + new CompositionBatch( + definition, + preparedCommands)); } if (batches.Count == 0) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 0e31370f0..75e526b77 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -14,19 +14,20 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; [GroupOutput("Drawing")] public class WebGPUDrawingBackendTests { - private static readonly (PixelColorBlendingMode ColorMode, PixelAlphaCompositionMode AlphaMode)[] GraphicsOptionsModePairs = - [ - (PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver), - (PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop), - (PixelColorBlendingMode.Add, PixelAlphaCompositionMode.Src), - (PixelColorBlendingMode.Subtract, PixelAlphaCompositionMode.DestOut), - (PixelColorBlendingMode.Screen, PixelAlphaCompositionMode.DestOver), - (PixelColorBlendingMode.Darken, PixelAlphaCompositionMode.DestAtop), - (PixelColorBlendingMode.Lighten, PixelAlphaCompositionMode.DestIn), - (PixelColorBlendingMode.Overlay, PixelAlphaCompositionMode.SrcIn), - (PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.Xor), - (PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Clear) - ]; + public static TheoryData GraphicsOptionsModePairs { get; } = + new() + { + { PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver }, + { PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop }, + { PixelColorBlendingMode.Add, PixelAlphaCompositionMode.Src }, + { PixelColorBlendingMode.Subtract, PixelAlphaCompositionMode.DestOut }, + { PixelColorBlendingMode.Screen, PixelAlphaCompositionMode.DestOver }, + { PixelColorBlendingMode.Darken, PixelAlphaCompositionMode.DestAtop }, + { PixelColorBlendingMode.Lighten, PixelAlphaCompositionMode.DestIn }, + { PixelColorBlendingMode.Overlay, PixelAlphaCompositionMode.SrcIn }, + { PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.Xor }, + { PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Clear } + }; [Theory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] @@ -232,101 +233,101 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test } [Theory] - [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] - public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput(TestImageProvider provider) + [WithBasicTestPatternImages(nameof(GraphicsOptionsModePairs), 384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput( + TestImageProvider provider, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode) where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); Brush brush = Brushes.Solid(Color.OrangeRed.WithAlpha(0.78F)); ImageComparer comparer = ImageComparer.TolerantPercentage(0.1F); - for (int i = 0; i < GraphicsOptionsModePairs.Length; i++) + + DrawingOptions drawingOptions = new() { - (PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) = GraphicsOptionsModePairs[i]; - DrawingOptions drawingOptions = new() + GraphicsOptions = new GraphicsOptions { - GraphicsOptions = new GraphicsOptions - { - Antialias = true, - BlendPercentage = 0.73F, - ColorBlendingMode = colorMode, - AlphaCompositionMode = alphaMode - } - }; - - using Image baseImage = provider.GetImage(); - using Image defaultImage = baseImage.Clone(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - defaultImage.DebugSave( - provider, - $"DefaultBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using Image webGpuImage = baseImage.Clone(); - using WebGPUDrawingBackend backend = new(); - Configuration webGpuConfiguration = Configuration.Default.Clone(); - webGpuConfiguration.SetDrawingBackend(backend); - webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); - webGpuImage.DebugSave( - provider, - $"WebGPUBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); - comparer.VerifySimilarity(defaultImage, webGpuImage); - } + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + defaultImage.DebugSave( + provider, + $"DefaultBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = baseImage.Clone(); + using WebGPUDrawingBackend backend = new(); + Configuration webGpuConfiguration = Configuration.Default.Clone(); + webGpuConfiguration.SetDrawingBackend(backend); + webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); + webGpuImage.DebugSave( + provider, + $"WebGPUBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + comparer.VerifySimilarity(defaultImage, webGpuImage); } [Theory] - [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] - public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput(TestImageProvider provider) + [WithBasicTestPatternImages(nameof(GraphicsOptionsModePairs), 384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput( + TestImageProvider provider, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode) where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); ImageComparer comparer = ImageComparer.TolerantPercentage(0.1F); - for (int i = 0; i < GraphicsOptionsModePairs.Length; i++) + + DrawingOptions drawingOptions = new() { - (PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) = GraphicsOptionsModePairs[i]; - DrawingOptions drawingOptions = new() + GraphicsOptions = new GraphicsOptions { - GraphicsOptions = new GraphicsOptions - { - Antialias = true, - BlendPercentage = 0.73F, - ColorBlendingMode = colorMode, - AlphaCompositionMode = alphaMode - } - }; - - using Image foreground = provider.GetImage(); - Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); - - using Image baseImage = provider.GetImage(); - using Image defaultImage = baseImage.Clone(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - defaultImage.DebugSave( - provider, - $"DefaultBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using Image webGpuImage = baseImage.Clone(); - using WebGPUDrawingBackend backend = new(); - Configuration webGpuConfiguration = Configuration.Default.Clone(); - webGpuConfiguration.SetDrawingBackend(backend); - webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); - webGpuImage.DebugSave( - provider, - $"WebGPUBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); - comparer.VerifySimilarity(defaultImage, webGpuImage); - } + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); + defaultImage.DebugSave( + provider, + $"DefaultBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + using Image webGpuImage = baseImage.Clone(); + using WebGPUDrawingBackend backend = new(); + Configuration webGpuConfiguration = Configuration.Default.Clone(); + webGpuConfiguration.SetDrawingBackend(backend); + webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); + webGpuImage.DebugSave( + provider, + $"WebGPUBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + AssertCoverageExecutionAccounting(backend); + AssertGpuPathWhenRequired(backend); + comparer.VerifySimilarity(defaultImage, webGpuImage); } [Theory] @@ -610,6 +611,68 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi AssertGpuPathWhenRequired(backend); } + [Theory] + [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + GraphicsOptions clearOptions = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + }; + + const int glyphCount = 200; + string text = new('A', glyphCount); + Brush drawBrush = Brushes.Solid(Color.HotPink); + Brush clearBrush = Brushes.Solid(Color.White); + + using Image image = provider.GetImage(); + using WebGPUDrawingBackend backend = new(); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using (DrawingCanvas clearCanvas = new(configuration, GetFrameRegion(image))) + { + clearCanvas.Fill(clearBrush, clearOptions); + clearCanvas.Flush(); + } + + int computeBatchesBeforeDraw = backend.TestingComputePathBatchCount; + + using (DrawingCanvas canvas = new(configuration, GetFrameRegion(image))) + { + canvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + canvas.Flush(); + } + + AssertGpuPathWhenRequired(backend); + if (!backend.TestingIsGPUReady) + { + return; + } + + int computeBatchesFromDraw = backend.TestingComputePathBatchCount - computeBatchesBeforeDraw; + + Assert.True( + computeBatchesFromDraw > 0, + "Expected repeated-glyph draw batch to execute via tiled compute composition."); + } + private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backend) { Assert.Equal( From 1eb7d56b2f98834b15affa7590c46bc6838013b5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 25 Feb 2026 00:48:40 +1000 Subject: [PATCH 022/136] Introduce CompositionScene and planning API --- .../WebGPUDrawingBackend.TiledComposite.cs | 211 ++++++++++++++---- .../WebGPUDrawingBackend.cs | 50 ++++- .../Processing/Backends/CompositionScene.cs | 20 ++ .../Backends/CompositionScenePlanner.cs | 104 +++++++++ .../Backends/DefaultDrawingBackend.cs | 21 ++ .../Processing/Backends/IDrawingBackend.cs | 4 +- .../DrawingCanvasBatcher{TPixel}.cs | 120 +--------- .../Backends/SkiaCoverageDrawingBackend.cs | 64 +++--- .../Processing/DrawingCanvasBatcherTests.cs | 14 +- .../RasterizerDefaultsExtensionsTests.cs | 2 +- 10 files changed, 415 insertions(+), 195 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs index 4d5fcfdb0..8e25641b9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs @@ -17,6 +17,30 @@ internal sealed unsafe partial class WebGPUDrawingBackend private const int TiledCompositeTileSize = CompositeComputeWorkgroupSize; private const string TiledCompositePipelineKey = "tiled-composite"; + /// + /// Composes one brush command into GPU brush/source-layer data consumed by the tiled compute shader. + /// + /// The destination/source pixel format. + private interface ITiledCompositeBrushComposer + where TPixel : unmanaged, IPixel + { + /// + /// Converts one prepared command into brush data and source-layer bindings. + /// + /// The prepared command being encoded. + /// The active flush context for target-space mapping. + /// Shared build context accumulating source layers and brush data. + /// The encoded brush-data index for the command. + /// Failure reason when conversion cannot complete. + /// when conversion succeeds; otherwise . + bool TryCompose( + PreparedCompositionCommand command, + WebGPUFlushContext flushContext, + TiledCompositeBuildContext buildContext, + out int brushDataIndex, + out string? error); + } + private bool TryCompositeBatchTiled( WebGPUFlushContext flushContext, TextureView* coverageView, @@ -153,10 +177,7 @@ private bool TryRunTiledCompositeComputePass( Span tileCommandCounts = rentedTileCounts.AsSpan(0, tileCount); TiledCompositeCommandData[] commandData = new TiledCompositeCommandData[commandCount]; - List brushData = []; - List> sourceLayers = []; - Dictionary sourceImageLayers = new(ReferenceEqualityComparer.Instance); - Dictionary solidColorLayers = []; + TiledCompositeBuildContext buildContext = new(); try { @@ -182,55 +203,17 @@ private bool TryRunTiledCompositeComputePass( } } - int sourceLayer; - Rectangle sourceRegion; - int brushOriginX; - int brushOriginY; - if (command.Brush is ImageBrush imageBrush) + if (!TryGetBrushComposer(command.Brush, out ITiledCompositeBrushComposer composer)) { - Image sourceImage = (Image)imageBrush.SourceImage; - if (!sourceImageLayers.TryGetValue(sourceImage, out sourceLayer)) - { - sourceLayer = sourceLayers.Count; - sourceImageLayers.Add(sourceImage, sourceLayer); - sourceLayers.Add(TiledSourceLayer.CreateImage(sourceImage)); - } - - sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); - brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); - brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); + error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; + return false; } - else if (command.Brush is SolidBrush solidBrush) - { - TPixel solidPixel = solidBrush.Color.ToPixel(); - if (!solidColorLayers.TryGetValue(solidPixel, out sourceLayer)) - { - sourceLayer = sourceLayers.Count; - solidColorLayers.Add(solidPixel, sourceLayer); - sourceLayers.Add(TiledSourceLayer.CreateSolid(solidPixel)); - } - sourceRegion = new Rectangle(0, 0, 1, 1); - brushOriginX = 0; - brushOriginY = 0; - } - else + if (!composer.TryCompose(command, flushContext, buildContext, out int brushDataIndex, out error)) { - error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; return false; } - int brushDataIndex = brushData.Count; - brushData.Add( - new TiledCompositeBrushData( - sourceRegion.X, - sourceRegion.Y, - sourceRegion.Width, - sourceRegion.Height, - brushOriginX, - brushOriginY, - sourceLayer)); - GraphicsOptions options = command.GraphicsOptions; commandData[commandIndex] = new TiledCompositeCommandData( command.SourceOffset.X, @@ -280,7 +263,7 @@ private bool TryRunTiledCompositeComputePass( } } - if (!TryCreateSourceLayerTextureArray(flushContext, sourceLayers, out TextureView* sourceLayerView, out error) || + if (!TryCreateSourceLayerTextureArray(flushContext, buildContext.SourceLayers, out TextureView* sourceLayerView, out error) || !TryCreateAndUploadBuffer( flushContext, BufferUsage.Storage, @@ -305,7 +288,7 @@ private bool TryRunTiledCompositeComputePass( !TryCreateAndUploadBuffer( flushContext, BufferUsage.Storage, - CollectionsMarshal.AsSpan(brushData), + CollectionsMarshal.AsSpan(buildContext.BrushData), out WgpuBuffer* brushDataBuffer, out nuint brushDataBufferBytes, out error)) @@ -545,6 +528,28 @@ private static bool TryCreateSourceLayerTextureArray( return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetBrushComposer( + Brush brush, + out ITiledCompositeBrushComposer composer) + where TPixel : unmanaged, IPixel + { + if (brush is ImageBrush) + { + composer = ImageBrushTiledCompositeComposer.Instance; + return true; + } + + if (brush is SolidBrush) + { + composer = SolidBrushTiledCompositeComposer.Instance; + return true; + } + + composer = default!; + return false; + } + private static void UploadSolidSourceLayer( WebGPUFlushContext flushContext, Texture* texture, @@ -820,4 +825,114 @@ public TiledSourceLayer(TPixel solidPixel) public static TiledSourceLayer CreateSolid(TPixel solidPixel) => new(solidPixel); } + + private sealed class SolidBrushTiledCompositeComposer : ITiledCompositeBrushComposer + where TPixel : unmanaged, IPixel + { + public static SolidBrushTiledCompositeComposer Instance { get; } = new(); + + public bool TryCompose( + PreparedCompositionCommand command, + WebGPUFlushContext flushContext, + TiledCompositeBuildContext buildContext, + out int brushDataIndex, + out string? error) + { + SolidBrush solidBrush = (SolidBrush)command.Brush; + TPixel solidPixel = solidBrush.Color.ToPixel(); + int sourceLayer = buildContext.GetOrAddSolidLayer(solidPixel); + + brushDataIndex = buildContext.AddBrushData( + new TiledCompositeBrushData( + 0, + 0, + 1, + 1, + 0, + 0, + sourceLayer)); + + error = null; + return true; + } + } + + private sealed class ImageBrushTiledCompositeComposer : ITiledCompositeBrushComposer + where TPixel : unmanaged, IPixel + { + public static ImageBrushTiledCompositeComposer Instance { get; } = new(); + + public bool TryCompose( + PreparedCompositionCommand command, + WebGPUFlushContext flushContext, + TiledCompositeBuildContext buildContext, + out int brushDataIndex, + out string? error) + { + ImageBrush imageBrush = (ImageBrush)command.Brush; + Image sourceImage = (Image)imageBrush.SourceImage; + int sourceLayer = buildContext.GetOrAddImageLayer(sourceImage); + Rectangle sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); + int brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); + int brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); + + brushDataIndex = buildContext.AddBrushData( + new TiledCompositeBrushData( + sourceRegion.X, + sourceRegion.Y, + sourceRegion.Width, + sourceRegion.Height, + brushOriginX, + brushOriginY, + sourceLayer)); + + error = null; + return true; + } + } + + private sealed class TiledCompositeBuildContext + where TPixel : unmanaged, IPixel + { + private readonly Dictionary sourceImageLayers = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary solidColorLayers = []; + + public List BrushData { get; } = []; + + public List> SourceLayers { get; } = []; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int AddBrushData(in TiledCompositeBrushData brushData) + { + int index = this.BrushData.Count; + this.BrushData.Add(brushData); + return index; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOrAddSolidLayer(TPixel solidPixel) + { + if (!this.solidColorLayers.TryGetValue(solidPixel, out int sourceLayer)) + { + sourceLayer = this.SourceLayers.Count; + this.solidColorLayers.Add(solidPixel, sourceLayer); + this.SourceLayers.Add(TiledSourceLayer.CreateSolid(solidPixel)); + } + + return sourceLayer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOrAddImageLayer(Image sourceImage) + { + if (!this.sourceImageLayers.TryGetValue(sourceImage, out int sourceLayer)) + { + sourceLayer = this.SourceLayers.Count; + this.sourceImageLayers.Add(sourceImage, sourceLayer); + this.SourceLayers.Add(TiledSourceLayer.CreateImage(sourceImage)); + } + + return sourceLayer; + } + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 9b0cfabc2..62e666603 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -29,6 +29,7 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private bool isDisposed; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); + private static int nextSceneFlushId; public WebGPUDrawingBackend() => this.fallbackBackend = DefaultDrawingBackend.Instance; @@ -141,6 +142,51 @@ public bool IsCompositionBrushSupported(Brush brush) /// public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + if (compositionScene.Commands.Count == 0) + { + return; + } + + List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( + compositionScene.Commands, + target.Bounds); + if (preparedBatches.Count == 0) + { + return; + } + + bool supportsSharedFlush = true; + for (int i = 0; i < compositionScene.Commands.Count; i++) + { + if (!IsSupportedCompositionBrush(compositionScene.Commands[i].Brush)) + { + supportsSharedFlush = false; + break; + } + } + + int flushId = supportsSharedFlush ? Interlocked.Increment(ref nextSceneFlushId) : 0; + for (int i = 0; i < preparedBatches.Count; i++) + { + CompositionBatch batch = preparedBatches[i]; + this.FlushPreparedBatch( + configuration, + target, + new CompositionBatch( + batch.Definition, + batch.Commands, + flushId, + isFinalBatchInFlush: i == preparedBatches.Count - 1)); + } + } + + private void FlushPreparedBatch( Configuration configuration, ICanvasFrame target, CompositionBatch compositionBatch) @@ -301,7 +347,7 @@ private void FlushCompositionsFallback( { if (hasCpuRegion) { - this.fallbackBackend.FlushCompositions(configuration, target, compositionBatch); + this.fallbackBackend.FlushPreparedBatch(configuration, target, compositionBatch); return; } @@ -311,7 +357,7 @@ private void FlushCompositionsFallback( Buffer2DRegion stagingRegion = stagingLease.Region; ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); - this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionBatch); + this.fallbackBackend.FlushPreparedBatch(configuration, stagingFrame, compositionBatch); using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target); WebGPUFlushContext.UploadTextureFromRegion( diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs new file mode 100644 index 000000000..2393ce20a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One flush-time scene packet containing normalized composition commands in draw order. +/// +internal sealed class CompositionScene +{ + public CompositionScene(IReadOnlyList commands) + { + this.Commands = commands; + } + + /// + /// Gets normalized composition commands in submission order. + /// + public IReadOnlyList Commands { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs new file mode 100644 index 000000000..e337d54c8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Converts scene command streams into backend-ready prepared batches. +/// +internal static class CompositionScenePlanner +{ + /// + /// Creates contiguous prepared batches grouped by coverage definition key. + /// + /// Scene commands in submission order. + /// Target frame bounds in absolute coordinates. + /// Prepared contiguous batches ready for backend execution. + public static List CreatePreparedBatches( + IReadOnlyList commands, + in Rectangle targetBounds) + { + List batches = []; + int index = 0; + while (index < commands.Count) + { + CompositionCommand definitionCommand = commands[index]; + int definitionKey = definitionCommand.DefinitionKey; + List preparedCommands = []; + for (; index < commands.Count; index++) + { + CompositionCommand command = commands[index]; + if (command.DefinitionKey != definitionKey) + { + break; + } + + if (TryPrepareCommand(in command, in targetBounds, out PreparedCompositionCommand prepared)) + { + preparedCommands.Add(prepared); + } + } + + if (preparedCommands.Count == 0) + { + continue; + } + + CompositionCoverageDefinition definition = + new( + definitionKey, + definitionCommand.Path, + definitionCommand.RasterizerOptions); + + batches.Add(new CompositionBatch(definition, preparedCommands)); + } + + return batches; + } + + /// + /// Clips one scene command to target bounds and computes coverage source offset mapping. + /// + /// The source command. + /// Target frame bounds in absolute coordinates. + /// Prepared command when clipping produces visible output. + /// when the command has visible output in target bounds. + public static bool TryPrepareCommand( + in CompositionCommand command, + in Rectangle targetBounds, + out PreparedCompositionCommand prepared) + { + Rectangle interest = command.RasterizerOptions.Interest; + Rectangle commandDestination = new( + command.DestinationOffset.X + interest.X, + command.DestinationOffset.Y + interest.Y, + interest.Width, + interest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, commandDestination); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + prepared = default; + return false; + } + + Rectangle destinationLocalRegion = new( + clippedDestination.X - targetBounds.X, + clippedDestination.Y - targetBounds.Y, + clippedDestination.Width, + clippedDestination.Height); + + Point sourceOffset = new( + clippedDestination.X - commandDestination.X, + clippedDestination.Y - commandDestination.Y); + + prepared = new PreparedCompositionCommand( + destinationLocalRegion, + sourceOffset, + command.Brush, + command.BrushBounds, + command.GraphicsOptions); + + return true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index cb1a29484..379d05688 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -61,6 +61,27 @@ public void FillPath( /// public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TPixel : unmanaged, IPixel + { + if (compositionScene.Commands.Count == 0) + { + return; + } + + List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( + compositionScene.Commands, + target.Bounds); + + for (int i = 0; i < preparedBatches.Count; i++) + { + this.FlushPreparedBatch(configuration, target, preparedBatches[i]); + } + } + + internal void FlushPreparedBatch( Configuration configuration, ICanvasFrame target, CompositionBatch compositionBatch) diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index c5f319d31..5ccec307d 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -50,10 +50,10 @@ public void FillPath( /// The pixel format. /// Active processing configuration. /// Destination frame. - /// Prepared composition definitions and commands in batch order. + /// Scene commands in submission order. public void FlushCompositions( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch) + CompositionScene compositionScene) where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 444ee17f5..e34bc73f7 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -11,13 +11,12 @@ namespace SixLabors.ImageSharp.Drawing.Processing; ///
/// /// The batcher owns command buffering and normalization only; it does not rasterize or composite. -/// During flush it groups consecutive commands sharing the same coverage definition into a single -/// so backends rasterize once and apply multiple brushes in order. +/// During flush it emits a so each backend can plan execution +/// (for example: CPU batching or GPU tiling) without changing the canvas call surface. /// internal sealed class DrawingCanvasBatcher where TPixel : unmanaged, IPixel { - private static int nextFlushId; private readonly Configuration configuration; private readonly IDrawingBackend backend; private readonly ICanvasFrame targetFrame; @@ -45,17 +44,11 @@ public void AddComposition(in CompositionCommand composition) => this.commands.Add(composition); /// - /// Flushes queued commands to the backend, preserving submission order. + /// Flushes queued commands to the backend as one scene packet, preserving submission order. /// /// - /// This method performs only command normalization and grouping: - /// - /// Split the queue into contiguous runs of matching . - /// Clip each run command to the target frame bounds. - /// Compute so clipped destination pixels map to the correct coverage pixels. - /// Send one per contiguous run. - /// - /// The backend then rasterizes coverage once per batch definition and composites commands in order. + /// Backends are responsible for planning execution (for example: grouping by coverage, caching, + /// or GPU binning). The batcher only records scene commands and forwards them on flush. /// public void FlushCompositions() { @@ -66,107 +59,8 @@ public void FlushCompositions() try { - Rectangle targetBounds = this.targetFrame.Bounds; - int index = 0; - List batches = []; - while (index < this.commands.Count) - { - CompositionCommand definitionCommand = this.commands[index]; - int definitionKey = definitionCommand.DefinitionKey; - - // Build one batch for the contiguous run sharing the same coverage definition. - List preparedCommands = []; - for (; index < this.commands.Count; index++) - { - CompositionCommand command = this.commands[index]; - if (command.DefinitionKey != definitionKey) - { - break; - } - - Rectangle interest = command.RasterizerOptions.Interest; - Rectangle commandDestination = new( - command.DestinationOffset.X + interest.X, - command.DestinationOffset.Y + interest.Y, - interest.Width, - interest.Height); - - Rectangle clippedDestination = Rectangle.Intersect(targetBounds, commandDestination); - - // Off-target commands in this run are dropped before backend dispatch. - if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) - { - continue; - } - - Rectangle destinationLocalRegion = new( - clippedDestination.X - targetBounds.X, - clippedDestination.Y - targetBounds.Y, - clippedDestination.Width, - clippedDestination.Height); - - Point sourceOffset = new( - clippedDestination.X - commandDestination.X, - clippedDestination.Y - commandDestination.Y); - - // Keep command ordering exactly as submitted. - preparedCommands.Add( - new PreparedCompositionCommand( - destinationLocalRegion, - sourceOffset, - command.Brush, - command.BrushBounds, - command.GraphicsOptions)); - } - - if (preparedCommands.Count == 0) - { - continue; - } - - CompositionCoverageDefinition definition = - new( - definitionKey, - definitionCommand.Path, - definitionCommand.RasterizerOptions); - - batches.Add( - new CompositionBatch( - definition, - preparedCommands)); - } - - if (batches.Count == 0) - { - return; - } - - // Use one shared flush id only when all queued brushes are directly supported by - // the active backend. If any brush is unsupported, backends receive independent - // batches (flushId = 0) so they can route each batch safely without shared state. - bool supportsSharedFlush = true; - for (int i = 0; i < this.commands.Count; i++) - { - if (!this.backend.IsCompositionBrushSupported(this.commands[i].Brush)) - { - supportsSharedFlush = false; - break; - } - } - - int flushId = supportsSharedFlush ? Interlocked.Increment(ref nextFlushId) : 0; - for (int i = 0; i < batches.Count; i++) - { - CompositionBatch batch = batches[i]; - this.backend.FlushCompositions( - this.configuration, - this.targetFrame, - new CompositionBatch( - batch.Definition, - batch.Commands, - flushId, - isFinalBatchInFlush: i == batches.Count - 1)); - } + CompositionScene scene = new(this.commands.ToArray()); + this.backend.FlushCompositions(this.configuration, this.targetFrame, scene); } finally { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 3fc4441a0..0ccca3407 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -49,41 +49,53 @@ public void FillPath( public void FlushCompositions( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch) + CompositionScene compositionScene) where TPixel : unmanaged, IPixel { - if (compositionBatch.Commands.Count == 0) + if (compositionScene.Commands.Count == 0) { return; } - CompositionCoverageDefinition definition = compositionBatch.Definition; - DrawingCoverageHandle coverageHandle = this.PrepareCoverage( - definition.Path, - definition.RasterizerOptions, - configuration.MemoryAllocator, - CoveragePreparationMode.Default); - try + List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( + compositionScene.Commands, + target.Bounds); + for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) { - IReadOnlyList commands = compositionBatch.Commands; - for (int i = 0; i < commands.Count; i++) + CompositionBatch compositionBatch = preparedBatches[batchIndex]; + if (compositionBatch.Commands.Count == 0) { - PreparedCompositionCommand composition = commands[i]; - ICanvasFrame commandTarget = new CanvasRegionFrame(target, composition.DestinationRegion); - - this.CompositeCoverage( - configuration, - commandTarget, - coverageHandle, - composition.SourceOffset, - composition.Brush, - composition.GraphicsOptions, - composition.BrushBounds); + continue; + } + + CompositionCoverageDefinition definition = compositionBatch.Definition; + DrawingCoverageHandle coverageHandle = this.PrepareCoverage( + definition.Path, + definition.RasterizerOptions, + configuration.MemoryAllocator, + CoveragePreparationMode.Default); + try + { + IReadOnlyList commands = compositionBatch.Commands; + for (int i = 0; i < commands.Count; i++) + { + PreparedCompositionCommand composition = commands[i]; + ICanvasFrame commandTarget = new CanvasRegionFrame(target, composition.DestinationRegion); + + this.CompositeCoverage( + configuration, + commandTarget, + coverageHandle, + composition.SourceOffset, + composition.Brush, + composition.GraphicsOptions, + composition.BrushBounds); + } + } + finally + { + this.ReleaseCoverage(coverageHandle); } - } - finally - { - this.ReleaseCoverage(coverageHandle); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 1acdee747..51001ed70 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -98,12 +98,20 @@ public bool IsCompositionBrushSupported(Brush brush) public void FlushCompositions( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch) + CompositionScene compositionScene) where TPixel : unmanaged, IPixel { - this.LastBatch = compositionBatch; + List batches = CompositionScenePlanner.CreatePreparedBatches( + compositionScene.Commands, + target.Bounds); + if (batches.Count == 0) + { + return; + } + + this.LastBatch = batches[batches.Count - 1]; this.HasBatch = true; - this.Batches.Add(compositionBatch); + this.Batches.AddRange(batches); } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index e1909c278..c6e83feb7 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -127,7 +127,7 @@ public void FillPath( public void FlushCompositions( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch) + CompositionScene compositionScene) where TPixel : unmanaged, IPixel { } From a0eccddd5386c9af04f1e216a3f07324d4ca152a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 25 Feb 2026 09:02:12 +1000 Subject: [PATCH 023/136] Document and refactor WebGPU drawing backend --- .../Shaders/CompositeDestinationBlitShader.cs | Bin 1828 -> 2630 bytes .../Shaders/CompositeDestinationInitShader.cs | Bin 1036 -> 1929 bytes .../Shaders/CoverageRasterizationShader.cs | Bin 1484 -> 2112 bytes .../Shaders/TiledCompositeComputeShader.cs | 20 ++- .../WEBGPU_BACKEND_PROCESS.md | 61 +++++++ .../WebGPUDrawingBackend.CompositePixels.cs | 32 ++++ .../WebGPUDrawingBackend.TiledComposite.cs | 100 ++++++++++++ .../WebGPUDrawingBackend.cs | 151 ++++++++++++++---- .../WebGPUFlushContext.cs | 14 -- .../Backends/DefaultDrawingBackend.cs | 46 +++++- .../DrawingCanvasBatcher{TPixel}.cs | 4 - 11 files changed, 377 insertions(+), 51 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs index a9a900a311c0ee9359c1f93324e1847ae83d3e88..3f641e9ab83aa7f876e68df46383654c67be88e5 100644 GIT binary patch delta 892 zcmY+DzmC)}5XMDAfdmy2?dV#x7s@NbN))g{y5gj})74HgNv!jqXl(E8R_UREci;_p z2cC=1$!7n!iR^f0zWIIQKkxp%|Mhb$ih|0JtW~mKzsb9&&HXJ2yM~2Aukt4aZYX-H zjE+=$JNSIiaERKesJ){Qy{L|9&^tDPDpfy|b_j_*Q&iG%q{zFNgs-P6Wib^Um#U>O zBs_;%!vmtWEQmnnoR_VRffTjLm{1F|qbORr&M+wlaR_Lkz_N>n5UYkjtm^*sW7ad( zKZw6QSHG%K+oD~K-goq(W3N#LnQQM_-9dYU<2aKDj0=oh;IS|5%UbGzlvc-7vK0 zJdY3GRyQ-?;FHu$)jo2V`BwYs&Nab62w%ZF;+>_wXmvV?C!_vCAgwyp}!`MCv|IjOoOnYpP7$@zK3C5d?@3Q38{*{LZCNtFsfeo1Ds mLV&A6N@7W(f<|7xLPlz0fkI+VPJS{_DnCzCZ?is&I2!;>`W^BB diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs index d9a74c4e4bf8edff0fad80f673ca239664cafe19..4528fc4f67a782e96f1dea11ec4cd8e1f7b8ac07 100644 GIT binary patch literal 1929 zcmZuxTW{Mo6yCFc#etvfHMX*J4?&_B>6R@B5THTbpbtY~P}Gr3L?Ss}9II{q`wpo~ zq-KI)%M^9K+c``U_*3b|h;r{>!I!WVN4RI1GIl*n61W!}rNtb)%+Y|`<7fwLbtPQC zXFZ!qR-@I7$4y<8yPB1_-7}-tH-ximvx)z{ z+-~oIt6F=9VE3$Lz#IrEoM4ssh8A+PPDtj2lAy&AE3i%(!i2)8Ix^~I*nv&SR-=PWVR9x<0SD7ygvWfsr16G0cnZ$0~0q?W}R+jSqd2HF3|F z|BzN2QXi-m8Y>|whaO>+#jrH!8CEniY%{zQBBCZlLrQ~W8O&uB*(4;dJn;Yq&M-kx zB|N@6r3<95DvHt6p>dRf3=3r_SgMs;q=HPA&&uRibALWhHWXn|h~t>(lteoH`vtSx zhsUmSR<+7~g}^D{7`(8+i6D99_D1<~PXt5%9uPh}+!2;3!UVZJH9A+4+LJT{w+GEE zXbKuuy@qH!wT)IW_BH$^xypm$psC+QN{1#AGCm4nhzeIu)Hu}d{d_{yZRA}~6K%z# zidA9Ar_itLphRv#qge%6L|Hchn829UG}~d!htg>>wJJ>}0c)RC)T>>~exjN9D63|x znM^~EZK!{9gsV3G&=K?bJk}2P*CpAh7pF_O&P2qe`{C(<8Jfn>t~(Kqq>FxzcV~H8 zyg%E}5frL~>whZ!HmQ_$!+sLyb{A)MGalMqJc{fEFk@Jg9+3YYK_)2uBwSOkJw@_6 zOJ~~R`~>JTLM$Y3yQ`Uyi|fpbiVnO*S*eUwJ0TBUBs-BWpuo`fx-9qU#b zKt*R@E@}%`y#+UvzO*zrb}_bSc{w-|-W3H5sQLN`o2yuc?tc7u1ztD9gXU-CP{kk} zzH(am<<kev(q8nHSqPhh&zESGOzKOLF*^2<=2`q!Ud&R%B!0T#htVE_OC delta 85 zcmeC=@8OuxJ^2=+t*e4_er`c#PO5H6W^SrNa(-TMNn&1!LQ-OKc4~@3Ql$crUy_-u m5a6nil30?cpploakdc~Lppcl8lb;Nf%Fol(+uX(Sp9ugm(H_+R diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs index 5ee56afeab3fae5fa2989bc7a00a3f05f2b554f9..d3b6e11132678ed481fa1169db30eb1d2a004255 100644 GIT binary patch literal 2112 zcmcIl%Wm5^6z$qyap9GW`ZY-xLE;#lc4mSC0UFfFbWs!mO}f=^S?CvB;;;>qZLPE{Oc>mA5iAzVnp2*urjW$ znQqr%_5Nye4W?wiv-2RfZ9u@)9yL`3&5T7YK6_*ewoX(z&=<)?1x)6U0vl!w21e9l zF=(>VNF5(cY1GUOsg_JEV6Vkv|*%C0{;i6x>a8+fvMwGD+ptQP$MhR(6A8+7t zgrc!wnpvSFVI>w`XOAl3pnTMlReswF4!k1tG^R-U-GOtcK)RARqc0<%XDDFz^Zte~an{`vy7>Sb+gxsqK@|`WUa=Vfc5)QFP`19rsHPeN+ zx-FUYULjWz39oM=%{(bjV%CueCbk$|f_e^rORjQH*^j7;BC5I2$Ze1(Y;jdLqQaRK zHKzKVplq*1Za_OywMHOW6^|t1DQH^o?kSPwKM?2WKtIFAZ7VY0!GrH8^{uShO~d5M z`({ms%n>f*gnhzzJPyG8yRq7F3;&|F`1sb@##x9a^}GIg=})T_GHa9(-U}1bB)*-) z9{Gz^aeA_zrixE=L&QwVB`#DqtqTczbMFsm_OfH1Ex#&w5(-XWw*C>UJ=359-t$4& zMQY|j#E5wZ7%q>96Q3z8=gTSlF<(xG$r`QG@=)Wr_0;26(Ct>!5lKynVPWnMw@(&G z445XtzEb(nD{W^1)8MCvt`Fh65{D}yOQVZ1_k#W!9w#J$A4*)EgF8L>8Lc}B4Qa@< zX6=fw#!*e-be$yyy4k1gY(~;f!T(N|(z)RP>+glESpWZJuZdD|02-v^CbyEk>c_C)`d8z!kbniS?ciQwhd$IQC=C_=RBaq zeYM8C-WgvlisR+!*J$O`GO1s0J1081V>HrJt^VFWr8ka26j?H>L@xHf +/// WGSL compute shader for tiled brush composition over prepared path coverage. +///
+/// +/// The shader resolves tile-local command ranges, samples brush/source data, applies color blending +/// and Porter-Duff alpha composition, and writes updated destination pixels into storage. +/// internal static class TiledCompositeComputeShader { - // Compile-time constant backed by static PE data (no heap allocation). + /// + /// Gets the UTF-8 WGSL source bytes used by the tiled composite compute pipeline. + /// + /// + /// + /// The literal intentionally includes a trailing U+0000 null terminator before the suffix. + /// + /// + /// Native WebGPU shader creation expects WGSL as a null-terminated byte pointer. The explicit + /// terminator keeps shader bytes as a compile-time constant and avoids runtime append/copy overhead. + /// + /// public static ReadOnlySpan Code => """ struct CompositeCommand { diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md new file mode 100644 index 000000000..4dcf0dd93 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -0,0 +1,61 @@ +# WebGPU Backend Process + +This document describes the runtime flow used by `WebGPUDrawingBackend` when flushing a `CompositionScene`. + +## End-to-End Flow + +```text +DrawingCanvasBatcher.Flush() + -> IDrawingBackend.FlushCompositions(scene) + -> CompositionScenePlanner.CreatePreparedBatches(scene.Commands) + -> foreach prepared batch + -> WebGPUDrawingBackend.FlushPreparedBatch(batch) + -> validate brush support + pixel format support + -> acquire WebGPUFlushContext + -> shared session context when scene uses GPU-only brushes + -> standalone context otherwise + -> prepare/reuse GPU coverage texture for batch definition + -> composite commands (tiled compute path) + -> build tile ranges + tile command indices + -> build brush/source layer payloads + -> upload command/brush/tile buffers + -> dispatch compute workgroups + -> optional destination blit to target texture + -> finalize + -> submit GPU commands + -> readback to CPU region when target requires readback + -> on any GPU failure path: execute batch through DefaultDrawingBackend +``` + +## Context and Resource Lifetime + +- `WebGPUFlushContext` owns per-flush transient resources: + - command encoder + - bind groups + - transient buffers and textures + - optional readback buffer mapping sequence +- shared flush sessions are keyed by scene flush id: + - destination initialization is performed once + - destination storage buffer is reused across all session batches + - session is closed on final batch or on failure + +## Fallback Behavior + +Fallback is batch-scoped, not scene-scoped: + +- if target exposes a CPU region: + - run `DefaultDrawingBackend.FlushPreparedBatch(...)` directly +- if target is native-surface only: + - rent CPU staging frame + - run `DefaultDrawingBackend.FlushPreparedBatch(...)` on staging + - upload staging pixels back to native target texture + +## Shader Source and Null Terminator + +All WGSL sources in this backend are stored as UTF-8 compile-time literals with an explicit trailing U+0000. + +Reason: + +- native WebGPU module creation consumes WGSL through a null-terminated pointer +- embedding the terminator in the literal avoids runtime append/copy work +- keeping shader bytes as static literal data removes per-call allocations diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index 9f4c3d8c5..ad01234a2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -10,8 +10,18 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Pixel-format registration for composite session I/O. /// +/// +/// The map defined by is intentionally explicit and only +/// includes one-to-one format mappings where the GPU texture format can round-trip the pixel payload +/// without channel swizzle or custom conversion logic. +/// internal sealed partial class WebGPUDrawingBackend { + /// + /// Builds the static registration table that maps implementations to + /// compatible WebGPU storage/sampling formats. + /// + /// The registration map used during flush dispatch. private static Dictionary CreateCompositePixelHandlers() => // No-swizzle mappings only. Unsupported types are intentionally omitted from this map. @@ -43,6 +53,14 @@ private static Dictionary CreateCompositePixel [typeof(Rgba64)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Uint) }; + /// + /// Resolves the WebGPU texture format identifier for when supported. + /// + /// The requested pixel type. + /// Receives the mapped texture format identifier on success. + /// + /// when the pixel type is supported for GPU composition; otherwise . + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) where TPixel : unmanaged, IPixel @@ -57,8 +75,17 @@ internal static bool TryGetCompositeTextureFormat(out WebGPUTextureForma return true; } + /// + /// Per-pixel registration payload consumed by GPU composition setup. + /// private readonly struct CompositePixelRegistration { + /// + /// Initializes a new instance of the struct. + /// + /// The registered pixel CLR type. + /// The matching WebGPU texture format. + /// The unmanaged pixel size in bytes. public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, int pixelSizeInBytes) { this.PixelType = pixelType; @@ -72,6 +99,11 @@ public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, i public int PixelSizeInBytes { get; } + /// + /// Creates a registration record for . + /// + /// The matching WebGPU texture format. + /// The initialized registration. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CompositePixelRegistration Create(TextureFormat textureFormat) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs index 8e25641b9..02f712a75 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs @@ -41,6 +41,18 @@ bool TryCompose( out string? error); } + /// + /// Composites one prepared batch using the tiled compute path. + /// + /// The destination pixel format. + /// The active flush context for the current frame target. + /// The prepared GPU coverage texture view. + /// The prepared composition commands to apply in order. + /// + /// Indicates whether destination storage should be blitted back to the target texture after this batch. + /// + /// Receives an error message when composition fails. + /// when composition succeeds; otherwise . private bool TryCompositeBatchTiled( WebGPUFlushContext flushContext, TextureView* coverageView, @@ -73,6 +85,7 @@ private bool TryCompositeBatchTiled( return false; } + // Reuse destination storage across batches in the same flush session when available. WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; if (destinationPixelsBuffer is null) @@ -91,6 +104,7 @@ private bool TryCompositeBatchTiled( return false; } + // When the target cannot be sampled directly, copy into a transient sampling texture first. CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); } @@ -146,6 +160,19 @@ private bool TryCompositeBatchTiled( return true; } + /// + /// Builds tiled command indirection buffers and dispatches the tiled composite compute shader. + /// + /// The destination pixel format. + /// The active flush context. + /// The prepared GPU coverage texture view. + /// The destination storage buffer. + /// The destination storage size in bytes. + /// The prepared composition commands. + /// The destination width. + /// The destination height. + /// Receives an error message when dispatch fails. + /// on success; otherwise . private bool TryRunTiledCompositeComputePass( WebGPUFlushContext flushContext, TextureView* coverageView, @@ -190,6 +217,7 @@ private bool TryRunTiledCompositeComputePass( continue; } + // First pass: count how many commands overlap each tile. int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); @@ -228,6 +256,7 @@ private bool TryRunTiledCompositeComputePass( brushDataIndex); } + // Convert command counts into prefix ranges for compact tile command lists. TiledCompositeTileRange[] tileRanges = new TiledCompositeTileRange[tileCount]; int totalTileCommandRefs = 0; for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) @@ -238,6 +267,7 @@ private bool TryRunTiledCompositeComputePass( totalTileCommandRefs = checked(totalTileCommandRefs + count); } + // Second pass: write per-tile command index lists. uint[] tileCommandIndices = new uint[Math.Max(totalTileCommandRefs, 1)]; for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) { @@ -319,6 +349,7 @@ private bool TryRunTiledCompositeComputePass( return false; } + // Bind all shader inputs in one bind group so each tile dispatch has fixed resource layout. BindGroupEntry* entries = stackalloc BindGroupEntry[8]; entries[0] = new BindGroupEntry { @@ -417,6 +448,15 @@ private bool TryRunTiledCompositeComputePass( } } + /// + /// Builds and uploads the source layer texture array referenced by brush data. + /// + /// The source pixel format. + /// The active flush context. + /// The source layers to upload. + /// Receives the created texture array view. + /// Receives an error message when creation or upload fails. + /// on success; otherwise . private static bool TryCreateSourceLayerTextureArray( WebGPUFlushContext flushContext, List> sourceLayers, @@ -487,6 +527,7 @@ private static bool TryCreateSourceLayerTextureArray( { if (sourceLayers.Count == 0) { + // Keep resource bindings valid even when no command produced a source layer. UploadSolidSourceLayer(flushContext, texture, default(TPixel), 0); } else @@ -528,6 +569,13 @@ private static bool TryCreateSourceLayerTextureArray( return true; } + /// + /// Resolves the brush composer that maps a command brush to tiled shader data. + /// + /// The destination/source pixel format. + /// The command brush. + /// Receives the matching composer when found. + /// when a composer exists; otherwise . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetBrushComposer( Brush brush, @@ -550,6 +598,9 @@ private static bool TryGetBrushComposer( return false; } + /// + /// Uploads one 1x1 solid-color source layer into the texture array. + /// private static void UploadSolidSourceLayer( WebGPUFlushContext flushContext, Texture* texture, @@ -583,6 +634,17 @@ private static void UploadSolidSourceLayer( in size); } + /// + /// Allocates one GPU buffer and uploads source data into it. + /// + /// The unmanaged element type. + /// The active flush context. + /// The target buffer usage flags. + /// The data to upload. + /// Receives the created buffer. + /// Receives the allocated byte size. + /// Receives an error message when creation fails. + /// on success; otherwise . private static bool TryCreateAndUploadBuffer( WebGPUFlushContext flushContext, BufferUsage usage, @@ -622,6 +684,9 @@ private static bool TryCreateAndUploadBuffer( return true; } + /// + /// Creates the bind-group layout used by . + /// private static bool TryCreateTiledCompositeBindGroupLayout( WebGPU api, Device* device, @@ -735,6 +800,9 @@ private static bool TryCreateTiledCompositeBindGroupLayout( return true; } + /// + /// Per-command payload consumed by . + /// [StructLayout(LayoutKind.Sequential)] private readonly struct TiledCompositeCommandData( int sourceOffsetX, @@ -762,6 +830,9 @@ private readonly struct TiledCompositeCommandData( public readonly int Padding1 = 0; } + /// + /// Per-tile range into the compact tile-command index array. + /// [StructLayout(LayoutKind.Sequential)] private readonly struct TiledCompositeTileRange(uint startIndex, uint count) { @@ -769,6 +840,9 @@ private readonly struct TiledCompositeTileRange(uint startIndex, uint count) public readonly uint Count = count; } + /// + /// Brush source sampling payload consumed by . + /// [StructLayout(LayoutKind.Sequential)] private readonly struct TiledCompositeBrushData( int sourceRegionX, @@ -789,6 +863,9 @@ private readonly struct TiledCompositeBrushData( public readonly int Padding0 = 0; } + /// + /// Global dispatch parameters consumed by . + /// [StructLayout(LayoutKind.Sequential)] private readonly struct TiledCompositeParameters( int destinationWidth, @@ -802,6 +879,9 @@ private readonly struct TiledCompositeParameters( public readonly int TileSize = tileSize; } + /// + /// One tiled source layer entry, either sampled from an image or synthesized from a solid pixel. + /// private readonly struct TiledSourceLayer where TPixel : unmanaged, IPixel { @@ -826,11 +906,15 @@ public TiledSourceLayer(TPixel solidPixel) public static TiledSourceLayer CreateSolid(TPixel solidPixel) => new(solidPixel); } + /// + /// Brush composer for . + /// private sealed class SolidBrushTiledCompositeComposer : ITiledCompositeBrushComposer where TPixel : unmanaged, IPixel { public static SolidBrushTiledCompositeComposer Instance { get; } = new(); + /// public bool TryCompose( PreparedCompositionCommand command, WebGPUFlushContext flushContext, @@ -857,11 +941,15 @@ public bool TryCompose( } } + /// + /// Brush composer for . + /// private sealed class ImageBrushTiledCompositeComposer : ITiledCompositeBrushComposer where TPixel : unmanaged, IPixel { public static ImageBrushTiledCompositeComposer Instance { get; } = new(); + /// public bool TryCompose( PreparedCompositionCommand command, WebGPUFlushContext flushContext, @@ -891,6 +979,9 @@ public bool TryCompose( } } + /// + /// Mutable build context that accumulates deduplicated source layers and brush payloads per batch. + /// private sealed class TiledCompositeBuildContext where TPixel : unmanaged, IPixel { @@ -901,6 +992,9 @@ private sealed class TiledCompositeBuildContext public List> SourceLayers { get; } = []; + /// + /// Adds brush payload data and returns its index. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int AddBrushData(in TiledCompositeBrushData brushData) { @@ -909,6 +1003,9 @@ public int AddBrushData(in TiledCompositeBrushData brushData) return index; } + /// + /// Gets or creates a source layer index for a solid-color brush payload. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetOrAddSolidLayer(TPixel solidPixel) { @@ -922,6 +1019,9 @@ public int GetOrAddSolidLayer(TPixel solidPixel) return sourceLayer; } + /// + /// Gets or creates a source layer index for an image brush payload. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetOrAddImageLayer(Image sourceImage) { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 62e666603..311f1bd28 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -18,6 +18,33 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// WebGPU-backed implementation of . /// +/// +/// +/// This backend executes coverage generation and composition on WebGPU where possible and falls back to +/// when GPU execution is unavailable for a specific command set. +/// +/// +/// High-level flush pipeline: +/// +/// +/// CompositionScene +/// -> CompositionScenePlanner (prepared batches) +/// -> For each batch: +/// 1) Resolve pixel-format handler +/// 2) Acquire flush context (shared session when possible) +/// 3) Prepare/reuse GPU coverage for path definition +/// 4) Composite commands via tiled compute shader into destination pixel buffer +/// 5) Blit to target and optionally read back to CPU region +/// 6) On failure: delegate batch to DefaultDrawingBackend +/// +/// +/// Shared flush sessions allow multiple contiguous GPU-compatible batches to reuse destination initialization +/// and transient GPU resources for one scene flush. +/// +/// +/// See src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md for a full process walkthrough. +/// +/// internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const uint CompositeVertexCount = 6; @@ -31,6 +58,9 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); private static int nextSceneFlushId; + /// + /// Initializes a new instance of the class. + /// public WebGPUDrawingBackend() => this.fallbackBackend = DefaultDrawingBackend.Instance; @@ -95,6 +125,12 @@ public WebGPUDrawingBackend() ///
internal int TestingComputePathBatchCount { get; private set; } + /// + /// Attempts to expose native WebGPU device and queue handles for interop. + /// + /// Receives the device pointer when available. + /// Receives the queue pointer when available. + /// when both handles are available; otherwise . internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) { this.ThrowIfDisposed(); @@ -161,6 +197,7 @@ public void FlushCompositions( return; } + // Shared flush sessions are used only when every command brush is directly supported by GPU composition. bool supportsSharedFlush = true; for (int i = 0; i < compositionScene.Commands.Count; i++) { @@ -186,6 +223,13 @@ public void FlushCompositions( } } + /// + /// Executes one prepared composition batch, preferring GPU execution and falling back to CPU when required. + /// + /// The destination pixel format. + /// The active processing configuration. + /// The destination frame. + /// The prepared batch to execute. private void FlushPreparedBatch( Configuration configuration, ICanvasFrame target, @@ -220,6 +264,7 @@ private void FlushPreparedBatch( return; } + // Flush sessions keep destination state alive across batch boundaries for one scene flush. bool useFlushSession = compositionBatch.FlushId != 0; bool gpuSuccess = false; bool gpuReady = false; @@ -246,9 +291,9 @@ private void FlushPreparedBatch( out failure)) { gpuReady = true; - gpuSuccess = this.TryCompositeBatch( + gpuSuccess = this.TryCompositeBatchTiled( flushContext, - coverageEntry, + coverageEntry.GPUCoverageView, compositionBatch.Commands, blitToTarget: !useFlushSession || compositionBatch.IsFinalBatchInFlush, out failure); @@ -256,7 +301,7 @@ private void FlushPreparedBatch( { if (useFlushSession && !compositionBatch.IsFinalBatchInFlush) { - // Keep the render pass open for the next batch. + // Intermediate session batches defer final submit/readback until the last batch. } else { @@ -321,6 +366,9 @@ private void FlushPreparedBatch( this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); } + /// + /// Checks whether all prepared commands in the batch are directly composable by WebGPU. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) { @@ -335,9 +383,23 @@ private static bool AreAllCompositionBrushesSupported(IReadOnlyList + /// Checks whether the brush type is supported by the WebGPU composition path. + ///
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSupportedCompositionBrush(Brush brush) => brush is SolidBrush or ImageBrush; + /// + /// Executes one prepared batch on the CPU fallback backend. + /// + /// The destination pixel format. + /// The active processing configuration. + /// The original destination frame. + /// The prepared batch to execute. + /// + /// Indicates whether exposes CPU pixels directly. When , + /// a temporary staging frame is composed and uploaded to the native surface. + /// private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, @@ -367,6 +429,9 @@ private void FlushCompositionsFallback( stagingRegion); } + /// + /// Resolves (or creates) cached GPU coverage for the batch definition. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryPrepareGpuCoverage( WebGPUFlushContext flushContext, @@ -384,22 +449,9 @@ private static bool TryPrepareGpuCoverage( } } - private bool TryCompositeBatch( - WebGPUFlushContext flushContext, - WebGPUFlushContext.CoverageEntry coverageEntry, - IReadOnlyList commands, - bool blitToTarget, - out string? error) - where TPixel : unmanaged, IPixel - { - return this.TryCompositeBatchTiled( - flushContext, - coverageEntry.GPUCoverageView, - commands, - blitToTarget, - out error); - } - + /// + /// Allocates destination storage used by compute composition. + /// private static bool TryCreateDestinationPixelsBuffer( WebGPUFlushContext flushContext, int width, @@ -427,6 +479,9 @@ private static bool TryCreateDestinationPixelsBuffer( return true; } + /// + /// Initializes destination storage from the current destination texture contents. + /// private static bool TryInitializeDestinationPixels( WebGPUFlushContext flushContext, TextureView* sourceTextureView, @@ -502,6 +557,9 @@ private static bool TryInitializeDestinationPixels( return true; } + /// + /// Writes composed destination storage back to the render target through a fullscreen blit. + /// private static bool TryBlitDestinationPixelsToTarget( WebGPUFlushContext flushContext, WgpuBuffer* destinationPixelsBuffer, @@ -598,6 +656,9 @@ private static bool TryBlitDestinationPixelsToTarget( return true; } + /// + /// Creates the bind-group layout used by destination initialization compute shader. + /// private static bool TryCreateDestinationInitBindGroupLayout( WebGPU api, Device* device, @@ -645,6 +706,9 @@ private static bool TryCreateDestinationInitBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by destination blit render shader. + /// private static bool TryCreateDestinationBlitBindGroupLayout( WebGPU api, Device* device, @@ -749,6 +813,9 @@ private static bool TryCreateCompositionTexture( return true; } + /// + /// Copies one texture region from source to destination texture. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CopyTextureRegion( WebGPUFlushContext flushContext, @@ -776,10 +843,16 @@ private static void CopyTextureRegion( flushContext.Api.CommandEncoderCopyTextureToTexture(flushContext.CommandEncoder, in source, in destination, in copySize); } + /// + /// Divides by and rounds up. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint DivideRoundUp(int value, int divisor) => (uint)((value + divisor - 1) / divisor); + /// + /// Finalizes one flush by submitting command buffers and optionally reading results back to CPU memory. + /// private bool TryFinalizeFlush( WebGPUFlushContext flushContext, Buffer2DRegion cpuRegion) @@ -794,6 +867,9 @@ private bool TryFinalizeFlush( return TrySubmit(flushContext); } + /// + /// Submits the current command encoder, if any. + /// private static bool TrySubmit(WebGPUFlushContext flushContext) { CommandEncoder* commandEncoder = flushContext.CommandEncoder; @@ -828,18 +904,9 @@ private static bool TrySubmit(WebGPUFlushContext flushContext) } } - private static bool TrySubmitBatch(WebGPUFlushContext flushContext) - { - flushContext.EndRenderPassIfOpen(); - if (!TrySubmit(flushContext)) - { - return false; - } - - flushContext.ResetInstanceBufferOffset(); - return true; - } - + /// + /// Copies target texture contents to the readback buffer and transfers bytes into destination CPU pixels. + /// private bool TryReadBackToCpuRegion(WebGPUFlushContext flushContext, Buffer2DRegion destinationRegion) where TPixel : unmanaged, IPixel { @@ -890,6 +957,9 @@ flushContext.ReadbackBuffer is null || destinationRegion); } + /// + /// Maps the readback buffer and copies pixel data into the destination region. + /// private bool TryReadBackBufferToRegion( WebGPUFlushContext flushContext, WgpuBuffer* readbackBuffer, @@ -909,6 +979,7 @@ private bool TryReadBackBufferToRegion( ReadOnlySpan sourceData = new(mappedData, readbackByteCount); int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); + // Fast path for contiguous full-width rows. if (destinationRegion.Rectangle.X == 0 && sourceRowBytes == destinationStrideBytes && TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) @@ -934,6 +1005,9 @@ private bool TryReadBackBufferToRegion( } } + /// + /// Maps a readback buffer for CPU access and returns the mapped pointer. + /// private bool TryMapReadBuffer( WebGPUFlushContext flushContext, WgpuBuffer* readbackBuffer, @@ -987,15 +1061,24 @@ public void Dispose() this.isDisposed = true; } + /// + /// Throws when this backend is disposed. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + /// + /// Returns whether the 2D buffer is backed by a single contiguous memory segment. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSingleMemory(Buffer2D buffer) where T : struct => buffer.MemoryGroup.Count == 1; + /// + /// Returns the single contiguous memory segment of the provided buffer when available. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memory) where T : struct @@ -1010,6 +1093,9 @@ private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memo return true; } + /// + /// Waits for a GPU callback signal, polling the device when the WGPU extension is available. + /// private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEventSlim signal) { Wgpu? extension = flushContext.RuntimeLease.WgpuExtension; @@ -1031,6 +1117,9 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.IsSet; } + /// + /// Destination blit parameters consumed by . + /// [StructLayout(LayoutKind.Sequential)] private readonly struct CompositeDestinationBlitParameters( int batchWidth, diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index a5f9e67b5..dffee547f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -333,11 +333,6 @@ public bool EnsureCommandEncoder() return this.CommandEncoder is not null; } - public bool BeginRenderPass() - { - return this.BeginRenderPass(this.TargetView); - } - /// /// Begins a render pass that targets the specified texture view. /// @@ -1024,18 +1019,9 @@ internal static void UploadTextureFromRegion( } } - public void ResetInstanceBufferOffset() - => this.InstanceBufferWriteOffset = 0; - - public void AdvanceInstanceBufferOffset(nuint newOffset) - => this.InstanceBufferWriteOffset = AlignToStorageBufferOffset(newOffset); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static nuint AlignToStorageBufferOffset(nuint value) => (value + 255) & ~(nuint)255; - internal sealed class DeviceSharedState : IDisposable { private readonly Dictionary coverageCache = []; diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 379d05688..c00146757 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -7,8 +7,32 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Default drawing backend. +/// CPU fallback backend that executes path coverage rasterization and brush composition directly against a CPU region. /// +/// +/// +/// This backend is the correctness baseline for all composition behavior. It is also used as the +/// fallback path by GPU backends when the target surface, pixel format, or brush command cannot be +/// executed directly on the GPU. +/// +/// +/// Flush execution is intentionally split: +/// +/// +/// +/// +/// +/// converts scene commands into prepared batches with . +/// +/// +/// +/// +/// +/// rasterizes one shared coverage map per batch and applies brushes in original command order. +/// +/// +/// +/// internal sealed class DefaultDrawingBackend : IDrawingBackend { /// @@ -81,6 +105,19 @@ public void FlushCompositions( } } + /// + /// Executes one prepared batch on the CPU. + /// + /// The destination pixel format. + /// The active processing configuration. + /// The destination frame. + /// + /// One prepared batch where all commands share the same coverage definition and differ only by brush/options. + /// + /// + /// This method is intentionally reusable so GPU backends can delegate unsupported batches + /// without reconstructing a full . + /// internal void FlushPreparedBatch( Configuration configuration, ICanvasFrame target, @@ -119,6 +156,7 @@ internal void FlushPreparedBatch( } } + // Iterate by row so we slice the already-rasterized coverage map once per command row. for (int row = 0; row < maxHeight; row++) { for (int i = 0; i < commandCount; i++) @@ -149,6 +187,12 @@ internal void FlushPreparedBatch( } } + /// + /// Rasterizes one batch coverage map into a dense floating-point buffer. + /// + /// The path and rasterizer options shared by every command in the batch. + /// The allocator used for temporary coverage storage. + /// The populated coverage map for the batch interest region. private Buffer2D CreateCoverageMap( in CompositionCoverageDefinition definition, MemoryAllocator allocator) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index e34bc73f7..ee6743c58 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -27,10 +27,6 @@ internal DrawingCanvasBatcher( IDrawingBackend backend, ICanvasFrame targetFrame) { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(backend, nameof(backend)); - Guard.NotNull(targetFrame, nameof(targetFrame)); - this.configuration = configuration; this.backend = backend; this.targetFrame = targetFrame; From 4bf20165b66b22d3197dc91010078c7554e2ba83 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 25 Feb 2026 13:10:17 +1000 Subject: [PATCH 024/136] Add composition bounds and refactor flush context --- .../Shaders/CompositeDestinationInitShader.cs | Bin 1929 -> 2317 bytes .../WebGPUDrawingBackend.TiledComposite.cs | 494 ++++++----- .../WebGPUDrawingBackend.cs | 245 +++++- .../WebGPUFlushContext.cs | 621 ++++++++----- .../Processing/Backends/CompositionBatch.cs | 13 +- .../Backends/WebGPUDrawingBackendTests.cs | 830 +++++++++++------- 6 files changed, 1391 insertions(+), 812 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs index 4528fc4f67a782e96f1dea11ec4cd8e1f7b8ac07..a557cdbb201ace95950411ad98a50bb11ec0f563 100644 GIT binary patch delta 499 zcmZuty-&hG6rWlX9Z1|EFS_(%A*O67>~4fvZH{ubFX>0TUbPJcM#I2#{|J+RpI(mu zEq&|n`)xbB+kI(lQo%D%z)Or`N;!>~U_6E8HJsg?3NLWKaUp^c;Lx#K(14*Ih!!|a zGVU=q;pk)R<_*TZim6kI24y90jGpa~W9S#<&smHg36Ca#|HVg*`tsBGd8_Ke7_y;b zMKre*ad5W8hDSo4j;5+d$OPye?K+Jqq3oJ>J)KaN;D^bahAu7!1x&^V3;Ax&jEW+E m&Gn`hq{7ryo$Bjzcu+64PM$fb|65C2#l?lVAX+V7ulj%1_>i*z delta 122 zcmeAb>g3;0!#cT`twuE^Gq+g5R-q)dqNKDa)g?1GHLo}`Kd)G$IKQ+gIW;~rH!(eR zvJbnGJV>Qpg+fh@LV8YqQesYgW{O^=f}O1bSfX-r4|@~(^f#zq /// The prepared command being encoded. /// The active flush context for target-space mapping. /// Shared build context accumulating source layers and brush data. + /// + /// The destination-local composition bounds used to map brush-space origins into destination-buffer space. + /// /// The encoded brush-data index for the command. /// Failure reason when conversion cannot complete. /// when conversion succeeds; otherwise . - bool TryCompose( + public bool TryCompose( PreparedCompositionCommand command, WebGPUFlushContext flushContext, TiledCompositeBuildContext buildContext, + in Rectangle compositionBounds, out int brushDataIndex, out string? error); } @@ -48,6 +52,7 @@ bool TryCompose( /// The active flush context for the current frame target. /// The prepared GPU coverage texture view. /// The prepared composition commands to apply in order. + /// The destination-local bounds to initialize/compose/read back for this batch. /// /// Indicates whether destination storage should be blitted back to the target texture after this batch. /// @@ -57,6 +62,7 @@ private bool TryCompositeBatchTiled( WebGPUFlushContext flushContext, TextureView* coverageView, IReadOnlyList commands, + Rectangle? compositionBounds, bool blitToTarget, out string? error) where TPixel : unmanaged, IPixel @@ -67,7 +73,10 @@ private bool TryCompositeBatchTiled( return true; } - Rectangle targetLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); + Rectangle frameLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); + Rectangle targetLocalBounds = compositionBounds is Rectangle requestedBounds + ? Rectangle.Intersect(frameLocalBounds, requestedBounds) + : frameLocalBounds; if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) { return true; @@ -88,9 +97,19 @@ private bool TryCompositeBatchTiled( // Reuse destination storage across batches in the same flush session when available. WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; + if (destinationPixelsBuffer is not null && + (flushContext.CompositeDestinationWidth != targetLocalBounds.Width || + flushContext.CompositeDestinationHeight != targetLocalBounds.Height)) + { + error = "Mismatched composition bounds detected for a reused destination pixel buffer."; + return false; + } + if (destinationPixelsBuffer is null) { TextureView* sourceTextureView = flushContext.TargetView; + int sourceOriginX = targetLocalBounds.X; + int sourceOriginY = targetLocalBounds.Y; if (!flushContext.CanSampleTargetTexture) { if (!TryCreateCompositionTexture( @@ -106,6 +125,8 @@ private bool TryCompositeBatchTiled( // When the target cannot be sampled directly, copy into a transient sampling texture first. CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); + sourceOriginX = 0; + sourceOriginY = 0; } if (!TryCreateDestinationPixelsBuffer( @@ -119,8 +140,9 @@ private bool TryCompositeBatchTiled( flushContext, sourceTextureView, destinationPixelsBuffer, - targetLocalBounds.Width, - targetLocalBounds.Height, + targetLocalBounds, + sourceOriginX, + sourceOriginY, destinationPixelsByteSize, out error)) { @@ -129,6 +151,8 @@ private bool TryCompositeBatchTiled( flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; + flushContext.CompositeDestinationWidth = targetLocalBounds.Width; + flushContext.CompositeDestinationHeight = targetLocalBounds.Height; } if (!this.TryRunTiledCompositeComputePass( @@ -137,8 +161,7 @@ private bool TryCompositeBatchTiled( destinationPixelsBuffer, destinationPixelsByteSize, commands, - targetLocalBounds.Width, - targetLocalBounds.Height, + targetLocalBounds, out error)) { return false; @@ -169,8 +192,7 @@ private bool TryCompositeBatchTiled( /// The destination storage buffer. /// The destination storage size in bytes. /// The prepared composition commands. - /// The destination width. - /// The destination height. + /// The destination-local bounds covered by this composition pass. /// Receives an error message when dispatch fails. /// on success; otherwise . private bool TryRunTiledCompositeComputePass( @@ -179,8 +201,7 @@ private bool TryRunTiledCompositeComputePass( WgpuBuffer* destinationPixelsBuffer, nuint destinationPixelsByteSize, IReadOnlyList commands, - int destinationWidth, - int destinationHeight, + in Rectangle destinationBounds, out string? error) where TPixel : unmanaged, IPixel { @@ -191,6 +212,8 @@ private bool TryRunTiledCompositeComputePass( return true; } + int destinationWidth = destinationBounds.Width; + int destinationHeight = destinationBounds.Height; int tilesX = (destinationWidth + TiledCompositeTileSize - 1) / TiledCompositeTileSize; int tilesY = (destinationHeight + TiledCompositeTileSize - 1) / TiledCompositeTileSize; if (tilesX <= 0 || tilesY <= 0) @@ -199,253 +222,256 @@ private bool TryRunTiledCompositeComputePass( } int tileCount = checked(tilesX * tilesY); - int[] rentedTileCounts = ArrayPool.Shared.Rent(tileCount); - Array.Clear(rentedTileCounts, 0, tileCount); - Span tileCommandCounts = rentedTileCounts.AsSpan(0, tileCount); + using IMemoryOwner tileCommandCountsOwner = flushContext.MemoryAllocator.Allocate(tileCount, AllocationOptions.Clean); + Span tileCommandCounts = tileCommandCountsOwner.Memory.Span[..tileCount]; TiledCompositeCommandData[] commandData = new TiledCompositeCommandData[commandCount]; TiledCompositeBuildContext buildContext = new(); - try + for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) { - for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + PreparedCompositionCommand command = commands[commandIndex]; + Rectangle destinationRegion = command.DestinationRegion; + Rectangle localDestinationRegion = new( + destinationRegion.X - destinationBounds.X, + destinationRegion.Y - destinationBounds.Y, + destinationRegion.Width, + destinationRegion.Height); + + if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) { - PreparedCompositionCommand command = commands[commandIndex]; - Rectangle destinationRegion = command.DestinationRegion; - if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) - { - continue; - } - - // First pass: count how many commands overlap each tile. - int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); - int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); - int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); - int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); - for (int tileY = minTileY; tileY <= maxTileY; tileY++) - { - int rowStart = checked(tileY * tilesX); - for (int tileX = minTileX; tileX <= maxTileX; tileX++) - { - tileCommandCounts[rowStart + tileX]++; - } - } - - if (!TryGetBrushComposer(command.Brush, out ITiledCompositeBrushComposer composer)) - { - error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; - return false; - } - - if (!composer.TryCompose(command, flushContext, buildContext, out int brushDataIndex, out error)) - { - return false; - } - - GraphicsOptions options = command.GraphicsOptions; - commandData[commandIndex] = new TiledCompositeCommandData( - command.SourceOffset.X, - command.SourceOffset.Y, - destinationRegion.X, - destinationRegion.Y, - destinationRegion.Width, - destinationRegion.Height, - options.BlendPercentage, - (int)options.ColorBlendingMode, - (int)options.AlphaCompositionMode, - brushDataIndex); - } - - // Convert command counts into prefix ranges for compact tile command lists. - TiledCompositeTileRange[] tileRanges = new TiledCompositeTileRange[tileCount]; - int totalTileCommandRefs = 0; - for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) - { - int count = tileCommandCounts[tileIndex]; - tileRanges[tileIndex] = new TiledCompositeTileRange((uint)totalTileCommandRefs, (uint)count); - tileCommandCounts[tileIndex] = totalTileCommandRefs; - totalTileCommandRefs = checked(totalTileCommandRefs + count); + continue; } - // Second pass: write per-tile command index lists. - uint[] tileCommandIndices = new uint[Math.Max(totalTileCommandRefs, 1)]; - for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + // First pass: count how many commands overlap each tile. + int minTileX = Math.Clamp(localDestinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); + int minTileY = Math.Clamp(localDestinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); + int maxTileX = Math.Clamp((localDestinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); + int maxTileY = Math.Clamp((localDestinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); + for (int tileY = minTileY; tileY <= maxTileY; tileY++) { - Rectangle destinationRegion = commands[commandIndex].DestinationRegion; - if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) + int rowStart = checked(tileY * tilesX); + for (int tileX = minTileX; tileX <= maxTileX; tileX++) { - continue; - } - - int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); - int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); - int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); - int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); - for (int tileY = minTileY; tileY <= maxTileY; tileY++) - { - int rowStart = checked(tileY * tilesX); - for (int tileX = minTileX; tileX <= maxTileX; tileX++) - { - int tileIndex = rowStart + tileX; - int writeIndex = tileCommandCounts[tileIndex]++; - tileCommandIndices[writeIndex] = (uint)commandIndex; - } + tileCommandCounts[rowStart + tileX]++; } } - if (!TryCreateSourceLayerTextureArray(flushContext, buildContext.SourceLayers, out TextureView* sourceLayerView, out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - commandData.AsSpan(), - out WgpuBuffer* commandBuffer, - out nuint commandBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - tileRanges.AsSpan(), - out WgpuBuffer* tileRangeBuffer, - out nuint tileRangeBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - tileCommandIndices.AsSpan(), - out WgpuBuffer* tileCommandIndexBuffer, - out nuint tileCommandIndexBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - CollectionsMarshal.AsSpan(buildContext.BrushData), - out WgpuBuffer* brushDataBuffer, - out nuint brushDataBufferBytes, - out error)) + if (!TryGetBrushComposer(command.Brush, out ITiledCompositeBrushComposer composer)) { + error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; return false; } - TiledCompositeParameters parameters = new(destinationWidth, destinationHeight, tilesX, TiledCompositeTileSize); - if (!TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Uniform, - MemoryMarshal.CreateReadOnlySpan(ref parameters, 1), - out WgpuBuffer* parameterBuffer, - out nuint parameterBufferBytes, - out error)) + if (!composer.TryCompose(command, flushContext, buildContext, destinationBounds, out int brushDataIndex, out error)) { return false; } - if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - TiledCompositePipelineKey, - TiledCompositeComputeShader.Code, - TryCreateTiledCompositeBindGroupLayout, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, - out error)) - { - return false; - } + GraphicsOptions options = command.GraphicsOptions; + commandData[commandIndex] = new TiledCompositeCommandData( + command.SourceOffset.X, + command.SourceOffset.Y, + localDestinationRegion.X, + localDestinationRegion.Y, + localDestinationRegion.Width, + localDestinationRegion.Height, + options.BlendPercentage, + (int)options.ColorBlendingMode, + (int)options.AlphaCompositionMode, + brushDataIndex); + } - // Bind all shader inputs in one bind group so each tile dispatch has fixed resource layout. - BindGroupEntry* entries = stackalloc BindGroupEntry[8]; - entries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageView - }; - entries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = commandBuffer, - Offset = 0, - Size = commandBufferBytes - }; - entries[2] = new BindGroupEntry - { - Binding = 2, - Buffer = tileRangeBuffer, - Offset = 0, - Size = tileRangeBufferBytes - }; - entries[3] = new BindGroupEntry - { - Binding = 3, - Buffer = tileCommandIndexBuffer, - Offset = 0, - Size = tileCommandIndexBufferBytes - }; - entries[4] = new BindGroupEntry - { - Binding = 4, - Buffer = brushDataBuffer, - Offset = 0, - Size = brushDataBufferBytes - }; - entries[5] = new BindGroupEntry - { - Binding = 5, - TextureView = sourceLayerView - }; - entries[6] = new BindGroupEntry - { - Binding = 6, - Buffer = destinationPixelsBuffer, - Offset = 0, - Size = destinationPixelsByteSize - }; - entries[7] = new BindGroupEntry - { - Binding = 7, - Buffer = parameterBuffer, - Offset = 0, - Size = parameterBufferBytes - }; + // Convert command counts into prefix ranges for compact tile command lists. + TiledCompositeTileRange[] tileRanges = new TiledCompositeTileRange[tileCount]; + int totalTileCommandRefs = 0; + for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + int count = tileCommandCounts[tileIndex]; + tileRanges[tileIndex] = new TiledCompositeTileRange((uint)totalTileCommandRefs, (uint)count); + tileCommandCounts[tileIndex] = totalTileCommandRefs; + totalTileCommandRefs = checked(totalTileCommandRefs + count); + } - BindGroupDescriptor bindGroupDescriptor = new() + // Second pass: write per-tile command index lists. + uint[] tileCommandIndices = new uint[Math.Max(totalTileCommandRefs, 1)]; + for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + { + TiledCompositeCommandData command = commandData[commandIndex]; + Rectangle destinationRegion = new( + command.DestinationX, + command.DestinationY, + command.DestinationWidth, + command.DestinationHeight); + if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) { - Layout = bindGroupLayout, - EntryCount = 8, - Entries = entries - }; + continue; + } - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) + int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); + int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); + int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); + int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); + for (int tileY = minTileY; tileY <= maxTileY; tileY++) { - error = "Failed to create tiled composite bind group."; - return false; + int rowStart = checked(tileY * tilesX); + for (int tileX = minTileX; tileX <= maxTileX; tileX++) + { + int tileIndex = rowStart + tileX; + int writeIndex = tileCommandCounts[tileIndex]++; + tileCommandIndices[writeIndex] = (uint)commandIndex; + } } + } - flushContext.TrackBindGroup(bindGroup); + if (!TryCreateSourceLayerTextureArray(flushContext, buildContext.SourceLayers, out TextureView* sourceLayerView, out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + commandData.AsSpan(), + out WgpuBuffer* commandBuffer, + out nuint commandBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + tileRanges.AsSpan(), + out WgpuBuffer* tileRangeBuffer, + out nuint tileRangeBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + tileCommandIndices.AsSpan(), + out WgpuBuffer* tileCommandIndexBuffer, + out nuint tileCommandIndexBufferBytes, + out error) || + !TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Storage, + CollectionsMarshal.AsSpan(buildContext.BrushData), + out WgpuBuffer* brushDataBuffer, + out nuint brushDataBufferBytes, + out error)) + { + return false; + } - ComputePassDescriptor passDescriptor = default; - ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); - if (passEncoder is null) - { - error = "Failed to begin tiled composite compute pass."; - return false; - } + TiledCompositeParameters parameters = new(destinationWidth, destinationHeight, tilesX, TiledCompositeTileSize); + if (!TryCreateAndUploadBuffer( + flushContext, + BufferUsage.Uniform, + MemoryMarshal.CreateReadOnlySpan(ref parameters, 1), + out WgpuBuffer* parameterBuffer, + out nuint parameterBufferBytes, + out error)) + { + return false; + } - try - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, (uint)tilesX, (uint)tilesY, 1); - } - finally - { - flushContext.Api.ComputePassEncoderEnd(passEncoder); - flushContext.Api.ComputePassEncoderRelease(passEncoder); - } + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + TiledCompositePipelineKey, + TiledCompositeComputeShader.Code, + TryCreateTiledCompositeBindGroupLayout, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } - return true; + // Bind all shader inputs in one bind group so each tile dispatch has fixed resource layout. + BindGroupEntry* entries = stackalloc BindGroupEntry[8]; + entries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = coverageView + }; + entries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = commandBuffer, + Offset = 0, + Size = commandBufferBytes + }; + entries[2] = new BindGroupEntry + { + Binding = 2, + Buffer = tileRangeBuffer, + Offset = 0, + Size = tileRangeBufferBytes + }; + entries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = tileCommandIndexBuffer, + Offset = 0, + Size = tileCommandIndexBufferBytes + }; + entries[4] = new BindGroupEntry + { + Binding = 4, + Buffer = brushDataBuffer, + Offset = 0, + Size = brushDataBufferBytes + }; + entries[5] = new BindGroupEntry + { + Binding = 5, + TextureView = sourceLayerView + }; + entries[6] = new BindGroupEntry + { + Binding = 6, + Buffer = destinationPixelsBuffer, + Offset = 0, + Size = destinationPixelsByteSize + }; + entries[7] = new BindGroupEntry + { + Binding = 7, + Buffer = parameterBuffer, + Offset = 0, + Size = parameterBufferBytes + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 8, + Entries = entries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create tiled composite bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin tiled composite compute pass."; + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, (uint)tilesX, (uint)tilesY, 1); } finally { - ArrayPool.Shared.Return(rentedTileCounts); + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); } + + return true; } /// @@ -543,6 +569,7 @@ private static bool TryCreateSourceLayerTextureArray( flushContext.Queue, texture, sourceRegion, + flushContext.MemoryAllocator, 0, 0, (uint)i); @@ -656,7 +683,7 @@ private static bool TryCreateAndUploadBuffer( { nuint elementSize = (nuint)Unsafe.SizeOf(); nuint writeSize = checked((nuint)sourceData.Length * elementSize); - bufferSize = Math.Max(writeSize, Math.Max(elementSize, (nuint)16)); + bufferSize = Math.Max(writeSize, Math.Max(elementSize, 16)); BufferDescriptor descriptor = new() { @@ -919,12 +946,12 @@ public bool TryCompose( PreparedCompositionCommand command, WebGPUFlushContext flushContext, TiledCompositeBuildContext buildContext, + in Rectangle compositionBounds, out int brushDataIndex, out string? error) { SolidBrush solidBrush = (SolidBrush)command.Brush; - TPixel solidPixel = solidBrush.Color.ToPixel(); - int sourceLayer = buildContext.GetOrAddSolidLayer(solidPixel); + int sourceLayer = buildContext.GetOrAddSolidLayer(solidBrush); brushDataIndex = buildContext.AddBrushData( new TiledCompositeBrushData( @@ -954,6 +981,7 @@ public bool TryCompose( PreparedCompositionCommand command, WebGPUFlushContext flushContext, TiledCompositeBuildContext buildContext, + in Rectangle compositionBounds, out int brushDataIndex, out string? error) { @@ -961,8 +989,8 @@ public bool TryCompose( Image sourceImage = (Image)imageBrush.SourceImage; int sourceLayer = buildContext.GetOrAddImageLayer(sourceImage); Rectangle sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); - int brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X); - int brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y); + int brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X - compositionBounds.X); + int brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y - compositionBounds.Y); brushDataIndex = buildContext.AddBrushData( new TiledCompositeBrushData( @@ -987,6 +1015,8 @@ private sealed class TiledCompositeBuildContext { private readonly Dictionary sourceImageLayers = new(ReferenceEqualityComparer.Instance); private readonly Dictionary solidColorLayers = []; + private SolidBrush? lastSolidBrush; + private int lastSolidLayer; public List BrushData { get; } = []; @@ -1007,8 +1037,14 @@ public int AddBrushData(in TiledCompositeBrushData brushData) /// Gets or creates a source layer index for a solid-color brush payload. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOrAddSolidLayer(TPixel solidPixel) + public int GetOrAddSolidLayer(SolidBrush solidBrush) { + if (ReferenceEquals(this.lastSolidBrush, solidBrush)) + { + return this.lastSolidLayer; + } + + TPixel solidPixel = solidBrush.Color.ToPixel(); if (!this.solidColorLayers.TryGetValue(solidPixel, out int sourceLayer)) { sourceLayer = this.SourceLayers.Count; @@ -1016,6 +1052,8 @@ public int GetOrAddSolidLayer(TPixel solidPixel) this.SourceLayers.Add(TiledSourceLayer.CreateSolid(solidPixel)); } + this.lastSolidBrush = solidBrush; + this.lastSolidLayer = sourceLayer; return sourceLayer; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 311f1bd28..e212cca81 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -192,6 +191,7 @@ public void FlushCompositions( List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( compositionScene.Commands, target.Bounds); + if (preparedBatches.Count == 0) { return; @@ -209,9 +209,21 @@ public void FlushCompositions( } int flushId = supportsSharedFlush ? Interlocked.Increment(ref nextSceneFlushId) : 0; + Rectangle? sharedCompositionBounds = null; + if (supportsSharedFlush && TryGetCompositionBounds(preparedBatches, out Rectangle sceneBounds)) + { + sharedCompositionBounds = sceneBounds; + } + for (int i = 0; i < preparedBatches.Count; i++) { CompositionBatch batch = preparedBatches[i]; + Rectangle? compositionBounds = sharedCompositionBounds; + if (compositionBounds is null && TryGetCompositionBounds(batch.Commands, out Rectangle batchBounds)) + { + compositionBounds = batchBounds; + } + this.FlushPreparedBatch( configuration, target, @@ -219,7 +231,8 @@ public void FlushCompositions( batch.Definition, batch.Commands, flushId, - isFinalBatchInFlush: i == preparedBatches.Count - 1)); + isFinalBatchInFlush: i == preparedBatches.Count - 1, + compositionBounds)); } } @@ -280,8 +293,15 @@ private void FlushPreparedBatch( target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes, + configuration.MemoryAllocator, + compositionBatch.CompositionBounds, out hadExistingSession) - : WebGPUFlushContext.Create(target, pixelHandler.TextureFormat, pixelHandler.PixelSizeInBytes); + : WebGPUFlushContext.Create( + target, + pixelHandler.TextureFormat, + pixelHandler.PixelSizeInBytes, + configuration.MemoryAllocator, + compositionBatch.CompositionBounds); CompositionCoverageDefinition definition = compositionBatch.Definition; if (TryPrepareGpuCoverage( @@ -295,6 +315,7 @@ private void FlushPreparedBatch( flushContext, coverageEntry.GPUCoverageView, compositionBatch.Commands, + compositionBatch.CompositionBounds, blitToTarget: !useFlushSession || compositionBatch.IsFinalBatchInFlush, out failure); if (gpuSuccess) @@ -305,7 +326,7 @@ private void FlushPreparedBatch( } else { - gpuSuccess = this.TryFinalizeFlush(flushContext, cpuRegion); + gpuSuccess = this.TryFinalizeFlush(flushContext, cpuRegion, compositionBatch.CompositionBounds); } } } @@ -389,6 +410,45 @@ private static bool AreAllCompositionBrushesSupported(IReadOnlyList brush is SolidBrush or ImageBrush; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetCompositionBounds(IReadOnlyList commands, out Rectangle bounds) + { + if (commands.Count == 0) + { + bounds = default; + return false; + } + + Rectangle union = commands[0].DestinationRegion; + for (int i = 1; i < commands.Count; i++) + { + union = Rectangle.Union(union, commands[i].DestinationRegion); + } + + bounds = union; + return union.Width > 0 && union.Height > 0; + } + + private static bool TryGetCompositionBounds(List batches, out Rectangle bounds) + { + bool hasBounds = false; + Rectangle union = default; + + for (int i = 0; i < batches.Count; i++) + { + if (!TryGetCompositionBounds(batches[i].Commands, out Rectangle batchBounds)) + { + continue; + } + + union = hasBounds ? Rectangle.Union(union, batchBounds) : batchBounds; + hasBounds = true; + } + + bounds = union; + return hasBounds; + } + /// /// Executes one prepared batch on the CPU fallback backend. /// @@ -421,12 +481,31 @@ private void FlushCompositionsFallback( ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); this.fallbackBackend.FlushPreparedBatch(configuration, stagingFrame, compositionBatch); - using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target); - WebGPUFlushContext.UploadTextureFromRegion( - uploadContext.Api, - uploadContext.Queue, - uploadContext.TargetTexture, - stagingRegion); + using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target, configuration.MemoryAllocator); + if (compositionBatch.CompositionBounds is Rectangle uploadBounds && + uploadBounds.Width > 0 && + uploadBounds.Height > 0) + { + Buffer2DRegion uploadRegion = stagingRegion.GetSubRegion(uploadBounds); + WebGPUFlushContext.UploadTextureFromRegion( + uploadContext.Api, + uploadContext.Queue, + uploadContext.TargetTexture, + uploadRegion, + configuration.MemoryAllocator, + (uint)uploadBounds.X, + (uint)uploadBounds.Y, + 0); + } + else + { + WebGPUFlushContext.UploadTextureFromRegion( + uploadContext.Api, + uploadContext.Queue, + uploadContext.TargetTexture, + stagingRegion, + configuration.MemoryAllocator); + } } /// @@ -486,8 +565,9 @@ private static bool TryInitializeDestinationPixels( WebGPUFlushContext flushContext, TextureView* sourceTextureView, WgpuBuffer* destinationPixelsBuffer, - int destinationWidth, - int destinationHeight, + in Rectangle destinationBounds, + int sourceOriginX, + int sourceOriginY, nuint destinationPixelsByteSize, out string? error) { @@ -502,7 +582,33 @@ private static bool TryInitializeDestinationPixels( return false; } - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; + BufferDescriptor paramsDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); + if (paramsBuffer is null) + { + error = "Failed to create destination initialization parameter buffer."; + return false; + } + + flushContext.TrackBuffer(paramsBuffer); + CompositeDestinationInitParameters parameters = new( + destinationBounds.Width, + destinationBounds.Height, + sourceOriginX, + sourceOriginY); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + paramsBuffer, + 0, + ¶meters, + (nuint)Unsafe.SizeOf()); + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[3]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -515,11 +621,18 @@ private static bool TryInitializeDestinationPixels( Offset = 0, Size = destinationPixelsByteSize }; + bindGroupEntries[2] = new BindGroupEntry + { + Binding = 2, + Buffer = paramsBuffer, + Offset = 0, + Size = (nuint)Unsafe.SizeOf() + }; BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 2, + EntryCount = 3, Entries = bindGroupEntries }; @@ -543,8 +656,8 @@ private static bool TryInitializeDestinationPixels( { flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - uint dispatchX = DivideRoundUp(destinationWidth, CompositeComputeWorkgroupSize); - uint dispatchY = DivideRoundUp(destinationHeight, CompositeComputeWorkgroupSize); + uint dispatchX = DivideRoundUp(destinationBounds.Width, CompositeComputeWorkgroupSize); + uint dispatchY = DivideRoundUp(destinationBounds.Height, CompositeComputeWorkgroupSize); flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, dispatchX, dispatchY, 1); } finally @@ -665,7 +778,7 @@ private static bool TryCreateDestinationInitBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -688,10 +801,21 @@ private static bool TryCreateDestinationInitBindGroupLayout( MinBindingSize = 0 } }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 2, + EntryCount = 3, Entries = entries }; @@ -855,13 +979,14 @@ private static uint DivideRoundUp(int value, int divisor) /// private bool TryFinalizeFlush( WebGPUFlushContext flushContext, - Buffer2DRegion cpuRegion) + Buffer2DRegion cpuRegion, + Rectangle? readbackBounds) where TPixel : unmanaged, IPixel { flushContext.EndRenderPassIfOpen(); if (flushContext.RequiresReadback) { - return this.TryReadBackToCpuRegion(flushContext, cpuRegion); + return this.TryReadBackToCpuRegion(flushContext, cpuRegion, readbackBounds); } return TrySubmit(flushContext); @@ -907,7 +1032,10 @@ private static bool TrySubmit(WebGPUFlushContext flushContext) /// /// Copies target texture contents to the readback buffer and transfers bytes into destination CPU pixels. /// - private bool TryReadBackToCpuRegion(WebGPUFlushContext flushContext, Buffer2DRegion destinationRegion) + private bool TryReadBackToCpuRegion( + WebGPUFlushContext flushContext, + Buffer2DRegion destinationRegion, + Rectangle? readbackBounds) where TPixel : unmanaged, IPixel { if (flushContext.TargetTexture is null || @@ -923,11 +1051,20 @@ flushContext.ReadbackBuffer is null || return false; } + Rectangle copyBounds = readbackBounds ?? new Rectangle(0, 0, destinationRegion.Width, destinationRegion.Height); + if (copyBounds.Width <= 0 || copyBounds.Height <= 0) + { + return true; + } + + uint copyBytesPerRow = checked((uint)copyBounds.Width * (uint)Unsafe.SizeOf()); + copyBytesPerRow = (copyBytesPerRow + 255U) & ~255U; + ImageCopyTexture source = new() { Texture = flushContext.TargetTexture, MipLevel = 0, - Origin = new Origin3D(0, 0, 0), + Origin = new Origin3D((uint)copyBounds.X, (uint)copyBounds.Y, 0), Aspect = TextureAspect.All }; @@ -937,12 +1074,12 @@ flushContext.ReadbackBuffer is null || Layout = new TextureDataLayout { Offset = 0, - BytesPerRow = flushContext.ReadbackBytesPerRow, - RowsPerImage = (uint)destinationRegion.Height + BytesPerRow = copyBytesPerRow, + RowsPerImage = (uint)copyBounds.Height } }; - Extent3D copySize = new((uint)destinationRegion.Width, (uint)destinationRegion.Height, 1); + Extent3D copySize = new((uint)copyBounds.Width, (uint)copyBounds.Height, 1); flushContext.Api.CommandEncoderCopyTextureToBuffer(flushContext.CommandEncoder, in source, in destination, in copySize); if (!TrySubmit(flushContext)) @@ -953,8 +1090,9 @@ flushContext.ReadbackBuffer is null || return this.TryReadBackBufferToRegion( flushContext, flushContext.ReadbackBuffer, - checked((int)flushContext.ReadbackBytesPerRow), - destinationRegion); + checked((int)copyBytesPerRow), + destinationRegion, + copyBounds); } /// @@ -964,11 +1102,12 @@ private bool TryReadBackBufferToRegion( WebGPUFlushContext flushContext, WgpuBuffer* readbackBuffer, int sourceRowBytes, - Buffer2DRegion destinationRegion) + Buffer2DRegion destinationRegion, + in Rectangle copyBounds) where TPixel : unmanaged { - int destinationRowBytes = checked(destinationRegion.Width * Unsafe.SizeOf()); - int readbackByteCount = checked(sourceRowBytes * destinationRegion.Height); + int destinationRowBytes = checked(copyBounds.Width * Unsafe.SizeOf()); + int readbackByteCount = checked(sourceRowBytes * copyBounds.Height); if (!this.TryMapReadBuffer(flushContext, readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) { return false; @@ -980,21 +1119,34 @@ private bool TryReadBackBufferToRegion( int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); // Fast path for contiguous full-width rows. - if (destinationRegion.Rectangle.X == 0 && - sourceRowBytes == destinationStrideBytes && + if (copyBounds.X == 0 && + copyBounds.Width == destinationRegion.Width && TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) { Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); - int destinationStart = checked(destinationRegion.Rectangle.Y * destinationStrideBytes); - int copyByteCount = checked(destinationStrideBytes * destinationRegion.Height); - sourceData[..copyByteCount].CopyTo(destinationBytes.Slice(destinationStart, copyByteCount)); + int destinationStart = checked((destinationRegion.Rectangle.Y + copyBounds.Y) * destinationStrideBytes); + int copyByteCount = checked(destinationStrideBytes * copyBounds.Height); + Span destinationSlice = destinationBytes.Slice(destinationStart, copyByteCount); + if (sourceRowBytes == destinationStrideBytes) + { + sourceData[..copyByteCount].CopyTo(destinationSlice); + return true; + } + + for (int y = 0; y < copyBounds.Height; y++) + { + sourceData.Slice(y * sourceRowBytes, destinationStrideBytes) + .CopyTo(destinationSlice.Slice(y * destinationStrideBytes, destinationStrideBytes)); + } + return true; } - for (int y = 0; y < destinationRegion.Height; y++) + for (int y = 0; y < copyBounds.Height; y++) { ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); - MemoryMarshal.Cast(sourceRow).CopyTo(destinationRegion.DangerousGetRowSpan(y)); + MemoryMarshal.Cast(sourceRow).CopyTo( + destinationRegion.DangerousGetRowSpan(copyBounds.Y + y).Slice(copyBounds.X, copyBounds.Width)); } return true; @@ -1117,6 +1269,25 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.IsSet; } + /// + /// Destination initialization parameters consumed by . + /// + [StructLayout(LayoutKind.Sequential)] + private readonly struct CompositeDestinationInitParameters( + int batchWidth, + int batchHeight, + int sourceOriginX, + int sourceOriginY) + { + public readonly int BatchWidth = batchWidth; + + public readonly int BatchHeight = batchHeight; + + public readonly int SourceOriginX = sourceOriginX; + + public readonly int SourceOriginY = sourceOriginY; + } + /// /// Destination blit parameters consumed by . /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index dffee547f..8f240c5de 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -34,7 +34,7 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable private bool ownsTargetTexture; private bool ownsTargetView; private bool ownsReadbackBuffer; - private byte[]? compositionInstanceScratchBuffer; + private DeviceSharedState.CpuTargetLease? cpuTargetLease; private readonly List transientBindGroups = []; private readonly List transientBuffers = []; private readonly List transientTextureViews = []; @@ -51,6 +51,7 @@ private WebGPUFlushContext( Queue* queue, in Rectangle targetBounds, TextureFormat textureFormat, + MemoryAllocator memoryAllocator, DeviceSharedState deviceState) { this.RuntimeLease = runtimeLease; @@ -59,6 +60,7 @@ private WebGPUFlushContext( this.Queue = queue; this.TargetBounds = targetBounds; this.TextureFormat = textureFormat; + this.MemoryAllocator = memoryAllocator; this.DeviceState = deviceState; } @@ -74,6 +76,11 @@ private WebGPUFlushContext( public TextureFormat TextureFormat { get; } + /// + /// Gets the allocator used for temporary CPU staging buffers in this flush context. + /// + public MemoryAllocator MemoryAllocator { get; } + public DeviceSharedState DeviceState { get; } public Texture* TargetTexture { get; private set; } @@ -110,6 +117,16 @@ private WebGPUFlushContext( /// public nuint CompositeDestinationPixelsByteSize { get; internal set; } + /// + /// Gets or sets the destination buffer width represented by . + /// + public int CompositeDestinationWidth { get; internal set; } + + /// + /// Gets or sets the destination buffer height represented by . + /// + public int CompositeDestinationHeight { get; internal set; } + public CommandEncoder* CommandEncoder { get; set; } public RenderPassEncoder* PassEncoder { get; private set; } @@ -117,7 +134,9 @@ private WebGPUFlushContext( public static WebGPUFlushContext Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, - int pixelSizeInBytes) + int pixelSizeInBytes, + MemoryAllocator memoryAllocator, + Rectangle? initialUploadBounds = null) where TPixel : unmanaged, IPixel { WebGPUSurfaceCapability? nativeCapability = TryGetNativeSurfaceCapability(frame, expectedTextureFormat); @@ -138,7 +157,7 @@ public static WebGPUFlushContext Create( textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); bounds = new Rectangle(0, 0, nativeCapability.Width, nativeCapability.Height); deviceState = GetOrCreateDeviceState(lease.Api, device); - context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, deviceState); + context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, memoryAllocator, deviceState); context.InitializeNativeTarget(nativeCapability); return context; } @@ -154,8 +173,8 @@ public static WebGPUFlushContext Create( } deviceState = GetOrCreateDeviceState(lease.Api, device); - context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, deviceState); - context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes); + context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, memoryAllocator, deviceState); + context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, initialUploadBounds); return context; } catch @@ -165,7 +184,7 @@ public static WebGPUFlushContext Create( } } - public static WebGPUFlushContext CreateUploadContext(ICanvasFrame frame) + public static WebGPUFlushContext CreateUploadContext(ICanvasFrame frame, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { WebGPUSurfaceCapability? nativeCapability = @@ -185,6 +204,7 @@ public static WebGPUFlushContext CreateUploadContext(ICanvasFrame( ICanvasFrame frame, TextureFormat expectedTextureFormat, int pixelSizeInBytes, + MemoryAllocator memoryAllocator, + Rectangle? initialUploadBounds, out bool fromCache) where TPixel : unmanaged, IPixel { @@ -248,7 +270,7 @@ public static WebGPUFlushContext GetOrCreateFlushSessionContext( } fromCache = false; - WebGPUFlushContext created = Create(frame, expectedTextureFormat, pixelSizeInBytes); + WebGPUFlushContext created = Create(frame, expectedTextureFormat, pixelSizeInBytes, memoryAllocator, initialUploadBounds); if (FlushSessionContexts.TryAdd(flushId, created)) { return created; @@ -420,100 +442,6 @@ public void TrackTexture(Texture* texture) } } - /// - /// Gets a flush-scoped scratch buffer for writing composition instance payload bytes. - /// - public Span GetCompositionInstanceScratchBuffer(int requiredLength) - { - if (requiredLength <= 0) - { - return Span.Empty; - } - - byte[]? current = this.compositionInstanceScratchBuffer; - if (current is null || current.Length < requiredLength) - { - if (current is not null) - { - ArrayPool.Shared.Return(current); - } - - this.compositionInstanceScratchBuffer = ArrayPool.Shared.Rent(requiredLength); - current = this.compositionInstanceScratchBuffer; - } - - return current.AsSpan(0, requiredLength); - } - - /// - /// Gets a texture view for the source image from this flush cache, creating and uploading it on first use. - /// - internal bool TryGetOrCreateSourceTextureView(Image sourceImage, out TextureView* textureView) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(sourceImage, nameof(sourceImage)); - - if (this.cachedSourceTextureViews.TryGetValue(sourceImage, out nint cachedHandle) && cachedHandle != 0) - { - textureView = (TextureView*)cachedHandle; - return true; - } - - return this.TryCreateAndCacheSourceTextureView(sourceImage, out textureView); - } - - /// - /// Uploads one source image into a transient GPU texture and stores the resulting view in the flush cache. - /// - private bool TryCreateAndCacheSourceTextureView(Image sourceImage, out TextureView* textureView) - where TPixel : unmanaged, IPixel - { - TextureDescriptor textureDescriptor = new() - { - Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)sourceImage.Width, (uint)sourceImage.Height, 1), - Format = this.TextureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* sourceTexture = this.Api.DeviceCreateTexture(this.Device, in textureDescriptor); - if (sourceTexture is null) - { - textureView = null; - return false; - } - - TextureViewDescriptor sourceViewDescriptor = new() - { - Format = this.TextureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* sourceView = this.Api.TextureCreateView(sourceTexture, in sourceViewDescriptor); - if (sourceView is null) - { - this.Api.TextureRelease(sourceTexture); - textureView = null; - return false; - } - - Buffer2DRegion sourceRegionPixels = new(sourceImage.Frames.RootFrame.PixelBuffer, sourceImage.Bounds); - UploadTextureFromRegion(this.Api, this.Queue, sourceTexture, sourceRegionPixels); - - this.TrackTexture(sourceTexture); - this.TrackTextureView(sourceView); - this.cachedSourceTextureViews[sourceImage] = (nint)sourceView; - textureView = sourceView; - return true; - } - public void Dispose() { if (this.disposed) @@ -538,6 +466,9 @@ public void Dispose() this.InstanceBufferWriteOffset = 0; + this.cpuTargetLease?.Dispose(); + this.cpuTargetLease = null; + if (this.ownsReadbackBuffer && this.ReadbackBuffer is not null) { this.Api.BufferRelease(this.ReadbackBuffer); @@ -578,12 +509,6 @@ public void Dispose() this.transientTextureViews.Clear(); this.transientTextures.Clear(); - if (this.compositionInstanceScratchBuffer is not null) - { - ArrayPool.Shared.Return(this.compositionInstanceScratchBuffer); - this.compositionInstanceScratchBuffer = null; - } - // Cache entries point to transient texture views that are released above. this.cachedSourceTextureViews.Clear(); @@ -592,6 +517,8 @@ public void Dispose() this.TargetTexture = null; this.CompositeDestinationPixelsBuffer = null; this.CompositeDestinationPixelsByteSize = 0; + this.CompositeDestinationWidth = 0; + this.CompositeDestinationHeight = 0; this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; this.RequiresReadback = false; @@ -800,75 +727,46 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability) this.ownsReadbackBuffer = false; } - private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int pixelSizeInBytes) + private void InitializeCpuTarget( + Buffer2DRegion cpuRegion, + int pixelSizeInBytes, + Rectangle? initialUploadBounds) where TPixel : unmanaged { int width = cpuRegion.Width; int height = cpuRegion.Height; - uint textureRowBytes = checked((uint)width * (uint)pixelSizeInBytes); - uint readbackRowBytes = AlignTo256(textureRowBytes); - ulong readbackByteCount = checked((ulong)readbackRowBytes * (uint)height); - - TextureDescriptor targetTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = this.TextureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* targetTexture = this.Api.DeviceCreateTexture(this.Device, in targetTextureDescriptor); - if (targetTexture is null) - { - throw new InvalidOperationException("Failed to create CPU flush target texture."); - } - - TextureViewDescriptor targetViewDescriptor = new() - { - Format = this.TextureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* targetView = this.Api.TextureCreateView(targetTexture, in targetViewDescriptor); - if (targetView is null) - { - this.Api.TextureRelease(targetTexture); - throw new InvalidOperationException("Failed to create CPU flush target view."); - } - - BufferDescriptor readbackDescriptor = new() - { - Usage = BufferUsage.MapRead | BufferUsage.CopyDst, - Size = readbackByteCount - }; - - WgpuBuffer* readbackBuffer = this.Api.DeviceCreateBuffer(this.Device, in readbackDescriptor); - if (readbackBuffer is null) - { - this.Api.TextureViewRelease(targetView); - this.Api.TextureRelease(targetTexture); - throw new InvalidOperationException("Failed to create CPU flush readback buffer."); - } + DeviceSharedState.CpuTargetLease lease = this.DeviceState.RentCpuTarget( + this.TextureFormat, + width, + height, + pixelSizeInBytes); + Texture* targetTexture = lease.TargetTexture; + TextureView* targetView = lease.TargetView; + WgpuBuffer* readbackBuffer = lease.ReadbackBuffer; + uint readbackRowBytes = lease.ReadbackBytesPerRow; + ulong readbackByteCount = lease.ReadbackByteCount; try { - UploadTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion); + if (initialUploadBounds is Rectangle uploadBounds && + uploadBounds.Width > 0 && + uploadBounds.Height > 0) + { + Buffer2DRegion uploadRegion = cpuRegion.GetSubRegion(uploadBounds); + UploadTextureFromRegion(this.Api, this.Queue, targetTexture, uploadRegion, this.MemoryAllocator, (uint)uploadBounds.X, (uint)uploadBounds.Y, 0); + } + else + { + UploadTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion, this.MemoryAllocator); + } } catch { - this.Api.BufferRelease(readbackBuffer); - this.Api.TextureViewRelease(targetView); - this.Api.TextureRelease(targetTexture); + lease.Dispose(); throw; } + this.cpuTargetLease = lease; this.TargetTexture = targetTexture; this.TargetView = targetView; this.ReadbackBuffer = readbackBuffer; @@ -876,9 +774,9 @@ private void InitializeCpuTarget(Buffer2DRegion cpuRegion, int p this.ReadbackByteCount = readbackByteCount; this.RequiresReadback = true; this.CanSampleTargetTexture = true; - this.ownsTargetTexture = true; - this.ownsTargetView = true; - this.ownsReadbackBuffer = true; + this.ownsTargetTexture = false; + this.ownsTargetView = false; + this.ownsReadbackBuffer = false; } private static WebGPUSurfaceCapability? TryGetNativeSurfaceCapability(ICanvasFrame frame, TextureFormat expectedTextureFormat) @@ -940,15 +838,17 @@ internal static void UploadTextureFromRegion( WebGPU api, Queue* queue, Texture* destinationTexture, - Buffer2DRegion sourceRegion) + Buffer2DRegion sourceRegion, + MemoryAllocator memoryAllocator) where TPixel : unmanaged - => UploadTextureFromRegion(api, queue, destinationTexture, sourceRegion, 0, 0, 0); + => UploadTextureFromRegion(api, queue, destinationTexture, sourceRegion, memoryAllocator, 0, 0, 0); internal static void UploadTextureFromRegion( WebGPU api, Queue* queue, Texture* destinationTexture, Buffer2DRegion sourceRegion, + MemoryAllocator memoryAllocator, uint destinationX, uint destinationY, uint destinationLayer) @@ -965,57 +865,60 @@ internal static void UploadTextureFromRegion( Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); - if (sourceRegion.Rectangle.X == 0 && - sourceRegion.Width == sourceRegion.Buffer.Width && - sourceRegion.Buffer.MemoryGroup.Count == 1) + if (sourceRegion.Buffer.MemoryGroup.Count == 1) { int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); - int sourceRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - nuint sourceByteCount = checked((nuint)(((long)sourceStrideBytes * (sourceRegion.Height - 1)) + sourceRowBytes)); + int directPathRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + directPathRowBytes; + long directPathPackedByteCount = (long)directPathRowBytes * sourceRegion.Height; - TextureDataLayout layout = new() + // For contiguous backing memory, avoid row packing unless the region is very sparse. + // This keeps the hot path allocation-free for common text and image workloads. + if (directByteCount <= directPathPackedByteCount * 2) { - Offset = 0, - BytesPerRow = (uint)sourceStrideBytes, - RowsPerImage = (uint)sourceRegion.Height - }; + int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.Width) + sourceRegion.Rectangle.X); + int startByteOffset = checked(startPixelIndex * pixelSizeInBytes); + int uploadByteCount = checked((int)directByteCount); + nuint uploadByteCountNuint = checked((nuint)uploadByteCount); - Span firstRow = sourceRegion.DangerousGetRowSpan(0); - fixed (TPixel* uploadPtr = firstRow) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, sourceByteCount, in layout, in writeSize); - } + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)sourceStrideBytes, + RowsPerImage = (uint)sourceRegion.Height + }; - return; + Memory sourceMemory = sourceRegion.Buffer.MemoryGroup[0]; + Span sourceBytes = MemoryMarshal.AsBytes(sourceMemory.Span).Slice(startByteOffset, uploadByteCount); + fixed (byte* uploadPtr = sourceBytes) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, uploadByteCountNuint, in layout, in writeSize); + } + + return; + } } int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); int packedByteCount = checked(packedRowBytes * sourceRegion.Height); - byte[] rented = ArrayPool.Shared.Rent(packedByteCount); - try + using IMemoryOwner packedOwner = memoryAllocator.Allocate(packedByteCount); + Span packedData = packedOwner.Memory.Span[..packedByteCount]; + for (int y = 0; y < sourceRegion.Height; y++) { - Span packedData = rented.AsSpan(0, packedByteCount); - for (int y = 0; y < sourceRegion.Height; y++) - { - ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); - MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); - } + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); + } - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)packedRowBytes, - RowsPerImage = (uint)sourceRegion.Height - }; + TextureDataLayout packedLayout = new() + { + Offset = 0, + BytesPerRow = (uint)packedRowBytes, + RowsPerImage = (uint)sourceRegion.Height + }; - fixed (byte* uploadPtr = packedData) - { - api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in layout, in writeSize); - } - } - finally + fixed (byte* uploadPtr = packedData) { - ArrayPool.Shared.Return(rented); + api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in packedLayout, in writeSize); } } @@ -1025,6 +928,7 @@ internal static void UploadTextureFromRegion( internal sealed class DeviceSharedState : IDisposable { private readonly Dictionary coverageCache = []; + private readonly ConcurrentDictionary cpuTargetCache = new(); private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); private WebGPURasterizer? coverageRasterizer; @@ -1050,6 +954,17 @@ internal DeviceSharedState(WebGPU api, Device* device) public int CoverageCount => this.coverageCache.Count; + public CpuTargetLease RentCpuTarget( + TextureFormat textureFormat, + int width, + int height, + int pixelSizeInBytes) + { + CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes); + CpuTargetEntry entry = this.cpuTargetCache.GetOrAdd(key, static _ => new CpuTargetEntry()); + return entry.Rent(this.Api, this.Device, in key); + } + public bool TryEnsureCoverageResources(out string? error) { if (this.disposed) @@ -1307,6 +1222,13 @@ public void Dispose() this.compositeComputePipelines.Clear(); + foreach (CpuTargetEntry entry in this.cpuTargetCache.Values) + { + entry.Dispose(this.Api); + } + + this.cpuTargetCache.Clear(); + this.disposed = true; } @@ -1556,6 +1478,295 @@ private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) } } + internal readonly struct CpuTargetCacheKey( + TextureFormat textureFormat, + int width, + int height, + int pixelSizeInBytes) : IEquatable + { + public TextureFormat TextureFormat { get; } = textureFormat; + + public int Width { get; } = width; + + public int Height { get; } = height; + + public int PixelSizeInBytes { get; } = pixelSizeInBytes; + + public bool Equals(CpuTargetCacheKey other) + => this.TextureFormat == other.TextureFormat && + this.Width == other.Width && + this.Height == other.Height && + this.PixelSizeInBytes == other.PixelSizeInBytes; + + public override bool Equals(object? obj) => obj is CpuTargetCacheKey other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes); + } + + internal sealed class CpuTargetEntry + { + private Texture* targetTexture; + private TextureView* targetView; + private WgpuBuffer* readbackBuffer; + private uint readbackBytesPerRow; + private ulong readbackByteCount; + private int inUse; + + internal CpuTargetLease Rent(WebGPU api, Device* device, in CpuTargetCacheKey key) + { + if (Interlocked.CompareExchange(ref this.inUse, 1, 0) == 0) + { + try + { + this.EnsureResources(api, device, in key); + } + catch + { + this.Release(); + throw; + } + + return new CpuTargetLease( + api, + this, + ownsResources: false, + this.targetTexture, + this.targetView, + this.readbackBuffer, + this.readbackBytesPerRow, + this.readbackByteCount); + } + + if (!TryCreateCpuTargetResources( + api, + device, + in key, + out Texture* temporaryTexture, + out TextureView* temporaryView, + out WgpuBuffer* temporaryReadbackBuffer, + out uint temporaryReadbackRowBytes, + out ulong temporaryReadbackByteCount)) + { + throw new InvalidOperationException("Failed to create temporary CPU flush target resources."); + } + + return new CpuTargetLease( + api, + owner: null, + ownsResources: true, + temporaryTexture, + temporaryView, + temporaryReadbackBuffer, + temporaryReadbackRowBytes, + temporaryReadbackByteCount); + } + + internal void Release() => Volatile.Write(ref this.inUse, 0); + + internal void Dispose(WebGPU api) + { + ReleaseCpuTargetResources(api, this.targetTexture, this.targetView, this.readbackBuffer); + this.targetTexture = null; + this.targetView = null; + this.readbackBuffer = null; + this.readbackBytesPerRow = 0; + this.readbackByteCount = 0; + this.inUse = 0; + } + + private void EnsureResources(WebGPU api, Device* device, in CpuTargetCacheKey key) + { + if (this.targetTexture is not null && + this.targetView is not null && + this.readbackBuffer is not null) + { + return; + } + + ReleaseCpuTargetResources(api, this.targetTexture, this.targetView, this.readbackBuffer); + this.targetTexture = null; + this.targetView = null; + this.readbackBuffer = null; + this.readbackBytesPerRow = 0; + this.readbackByteCount = 0; + + if (!TryCreateCpuTargetResources( + api, + device, + in key, + out this.targetTexture, + out this.targetView, + out this.readbackBuffer, + out this.readbackBytesPerRow, + out this.readbackByteCount)) + { + throw new InvalidOperationException("Failed to create cached CPU flush target resources."); + } + } + + private static bool TryCreateCpuTargetResources( + WebGPU api, + Device* device, + in CpuTargetCacheKey key, + out Texture* targetTexture, + out TextureView* targetView, + out WgpuBuffer* readbackBuffer, + out uint readbackBytesPerRow, + out ulong readbackByteCount) + { + targetTexture = null; + targetView = null; + readbackBuffer = null; + readbackBytesPerRow = 0; + readbackByteCount = 0; + + uint textureRowBytes = checked((uint)key.Width * (uint)key.PixelSizeInBytes); + readbackBytesPerRow = AlignTo256(textureRowBytes); + readbackByteCount = checked((ulong)readbackBytesPerRow * (uint)key.Height); + + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)key.Width, (uint)key.Height, 1), + Format = key.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + targetTexture = api.DeviceCreateTexture(device, in targetTextureDescriptor); + if (targetTexture is null) + { + return false; + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = key.TextureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + targetView = api.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + api.TextureRelease(targetTexture); + targetTexture = null; + return false; + } + + BufferDescriptor readbackDescriptor = new() + { + Usage = BufferUsage.MapRead | BufferUsage.CopyDst, + Size = readbackByteCount + }; + + readbackBuffer = api.DeviceCreateBuffer(device, in readbackDescriptor); + if (readbackBuffer is null) + { + api.TextureViewRelease(targetView); + api.TextureRelease(targetTexture); + targetView = null; + targetTexture = null; + return false; + } + + return true; + } + + private static void ReleaseCpuTargetResources( + WebGPU api, + Texture* targetTexture, + TextureView* targetView, + WgpuBuffer* readbackBuffer) + { + if (readbackBuffer is not null) + { + api.BufferRelease(readbackBuffer); + } + + if (targetView is not null) + { + api.TextureViewRelease(targetView); + } + + if (targetTexture is not null) + { + api.TextureRelease(targetTexture); + } + } + } + + public sealed class CpuTargetLease : IDisposable + { + private readonly WebGPU api; + private readonly CpuTargetEntry? owner; + private readonly bool ownsResources; + private int disposed; + + internal CpuTargetLease( + WebGPU api, + CpuTargetEntry? owner, + bool ownsResources, + Texture* targetTexture, + TextureView* targetView, + WgpuBuffer* readbackBuffer, + uint readbackBytesPerRow, + ulong readbackByteCount) + { + this.api = api; + this.owner = owner; + this.ownsResources = ownsResources; + this.TargetTexture = targetTexture; + this.TargetView = targetView; + this.ReadbackBuffer = readbackBuffer; + this.ReadbackBytesPerRow = readbackBytesPerRow; + this.ReadbackByteCount = readbackByteCount; + } + + public Texture* TargetTexture { get; } + + public TextureView* TargetView { get; } + + public WgpuBuffer* ReadbackBuffer { get; } + + public uint ReadbackBytesPerRow { get; } + + public ulong ReadbackByteCount { get; } + + public void Dispose() + { + if (Interlocked.Exchange(ref this.disposed, 1) != 0) + { + return; + } + + if (this.ownsResources) + { + if (this.ReadbackBuffer is not null) + { + this.api.BufferRelease(this.ReadbackBuffer); + } + + if (this.TargetView is not null) + { + this.api.TextureViewRelease(this.TargetView); + } + + if (this.TargetTexture is not null) + { + this.api.TextureRelease(this.TargetTexture); + } + } + + this.owner?.Release(); + } + } + private sealed class CompositePipelineInfrastructure { public Dictionary<(TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs index 217ad9a46..d05a451b2 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs @@ -12,12 +12,14 @@ public CompositionBatch( in CompositionCoverageDefinition definition, IReadOnlyList commands, int flushId = 0, - bool isFinalBatchInFlush = true) + bool isFinalBatchInFlush = true, + Rectangle? compositionBounds = null) { this.Definition = definition; this.Commands = commands; this.FlushId = flushId; this.IsFinalBatchInFlush = isFinalBatchInFlush; + this.CompositionBounds = compositionBounds; } /// @@ -39,4 +41,13 @@ public CompositionBatch( /// Gets a value indicating whether this is the last batch emitted for the current flush identifier. /// public bool IsFinalBatchInFlush { get; } + + /// + /// Gets the destination-local bounds touched by this batch or scene flush when known. + /// + /// + /// GPU backends can use this region to limit destination initialization, composition, and readback + /// to modified pixels. + /// + public Rectangle? CompositionBounds { get; } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 75e526b77..9c2fe2350 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -42,36 +42,44 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); - using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - defaultImage.DebugSave( - provider, - "DefaultBackend_FillPath", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + Action> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); - using Image webGpuImage = provider.GetImage(); - using WebGPUDrawingBackend backend = new(); - webGpuImage.Configuration.SetDrawingBackend(backend); - webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - webGpuImage.DebugSave( - provider, - "WebGPUBackend_FillPath", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - Assert.True(backend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); - Assert.Equal(0, backend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(backend); - if (backend.TestingIsGPUReady) - { - Assert.True(backend.TestingGPUPrepareCoverageCallCount > 0); - Assert.True(backend.TestingGPUCompositeCoverageCallCount + backend.TestingFallbackCompositeCoverageCallCount > 0); + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath", defaultImage, cpuRegionImage, nativeSurfaceImage); + + Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(cpuRegionBackend); + if (cpuRegionBackend.TestingIsGPUReady) + { + Assert.True(cpuRegionBackend.TestingGPUPrepareCoverageCallCount > 0); + Assert.True(cpuRegionBackend.TestingGPUCompositeCoverageCallCount + cpuRegionBackend.TestingFallbackCompositeCoverageCallCount > 0); } - ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); - comparer.VerifySimilarity(defaultImage, webGpuImage); + Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } [Theory] @@ -97,52 +105,49 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid using Image foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + Action> drawAction = canvas => + { + canvas.Fill(clearBrush, clearOptions); + canvas.FillPath(polygon, brush, drawingOptions); + }; using Image defaultImage = new(384, 256); - using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) - { - defaultCanvas.Fill(clearBrush, clearOptions); - defaultCanvas.FillPath(polygon, brush, drawingOptions); - defaultCanvas.Flush(); - } + RenderWithDefaultBackend(defaultImage, drawAction); - defaultImage.DebugSave( - provider, - "DefaultBackend_FillPath_ImageBrush", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + using Image cpuRegionImage = new(384, 256); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); - using Image webGpuImage = new(384, 256); - using WebGPUDrawingBackend backend = new(); - Configuration webGpuConfiguration = Configuration.Default.Clone(); - webGpuConfiguration.SetDrawingBackend(backend); + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction); - using (DrawingCanvas webGpuCanvas = new(webGpuConfiguration, GetFrameRegion(webGpuImage))) + DebugSaveBackendTriplet(provider, "FillPath_ImageBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); + + Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(cpuRegionBackend); + if (cpuRegionBackend.TestingIsGPUReady) { - webGpuCanvas.Fill(clearBrush, clearOptions); - webGpuCanvas.FillPath(polygon, brush, drawingOptions); - webGpuCanvas.Flush(); + Assert.True(cpuRegionBackend.TestingGPUCompositeCoverageCallCount > 0); } - webGpuImage.DebugSave( - provider, - "WebGPUBackend_FillPath_ImageBrush", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - Assert.True(backend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); - Assert.Equal(0, backend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(backend); - if (backend.TestingIsGPUReady) + Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + if (nativeSurfaceBackend.TestingIsGPUReady) { - Assert.True(backend.TestingGPUCompositeCoverageCallCount > 0); + Assert.True(nativeSurfaceBackend.TestingGPUCompositeCoverageCallCount > 0); } - AssertGpuPathWhenRequired(backend); - - ImageComparer comparer = ImageComparer.TolerantPercentage(1F); - comparer.VerifySimilarity(defaultImage, webGpuImage); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } [Theory] @@ -184,52 +189,44 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); + Action> drawAction = canvas => canvas.FillPath(path, brush, drawingOptions); using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); - defaultImage.DebugSave( - provider, - "DefaultBackend_FillPath_NonZeroNestedContours", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + RenderWithDefaultBackend(defaultImage, drawAction); - using Image webGpuImage = provider.GetImage(); - using WebGPUDrawingBackend backend = new(); - webGpuImage.Configuration.SetDrawingBackend(backend); - webGpuImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, path)); - webGpuImage.DebugSave( - provider, - "WebGPUBackend_FillPath_NonZeroNestedContours", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); - Assert.True(backend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); - Assert.Equal(0, backend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); - // WebGPU and CPU rasterization differ slightly on edge coverage quantization, - // but non-zero winding semantics must still match. - Assert.Equal(defaultImage[128, 128], webGpuImage[128, 128]); + DebugSaveBackendTriplet(provider, "FillPath_NonZeroNestedContours", defaultImage, cpuRegionImage, nativeSurfaceImage); - ImageComparer referenceComparer = ImageComparer.TolerantPercentage(0.5F); - defaultImage.CompareToReferenceOutput( - referenceComparer, - provider, - "FillPath_NonZeroNestedContours_Expected", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(cpuRegionBackend); - webGpuImage.CompareToReferenceOutput( - referenceComparer, - provider, - "FillPath_NonZeroNestedContours_Expected", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); + Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + + // Non-zero winding semantics must still match on an interior point. + Assert.Equal(defaultImage[128, 128], cpuRegionImage[128, 128]); + Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); - ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); - comparer.VerifySimilarity(defaultImage, webGpuImage); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } [Theory] @@ -242,7 +239,6 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); + using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - defaultImage.DebugSave( - provider, - $"DefaultBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + RenderWithDefaultBackend(defaultImage, drawAction); - using Image webGpuImage = baseImage.Clone(); - using WebGPUDrawingBackend backend = new(); - Configuration webGpuConfiguration = Configuration.Default.Clone(); - webGpuConfiguration.SetDrawingBackend(backend); - webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); - webGpuImage.DebugSave( - provider, - $"WebGPUBackend_FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + using Image cpuRegionImage = baseImage.Clone(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); - comparer.VerifySimilarity(defaultImage, webGpuImage); + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + baseImage); + + DebugSaveBackendTriplet( + provider, + $"FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + defaultImage, + cpuRegionImage, + nativeSurfaceImage); + + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.1F); } [Theory] @@ -289,7 +292,6 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput { RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); - ImageComparer comparer = ImageComparer.TolerantPercentage(0.1F); DrawingOptions drawingOptions = new() { @@ -304,30 +306,36 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + Action> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - defaultImage.Mutate(ctx => ctx.Fill(drawingOptions, brush, polygon)); - defaultImage.DebugSave( - provider, - $"DefaultBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + RenderWithDefaultBackend(defaultImage, drawAction); - using Image webGpuImage = baseImage.Clone(); - using WebGPUDrawingBackend backend = new(); - Configuration webGpuConfiguration = Configuration.Default.Clone(); - webGpuConfiguration.SetDrawingBackend(backend); - webGpuImage.Mutate(webGpuConfiguration, ctx => ctx.Fill(drawingOptions, brush, polygon)); - webGpuImage.DebugSave( - provider, - $"WebGPUBackend_FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); + using Image cpuRegionImage = baseImage.Clone(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); - comparer.VerifySimilarity(defaultImage, webGpuImage); + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + baseImage); + + DebugSaveBackendTriplet( + provider, + $"FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + defaultImage, + cpuRegionImage, + nativeSurfaceImage); + + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.1F); } [Theory] @@ -349,35 +357,42 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag string text = "Sphinx of black quartz, judge my vow\n0123456789"; Brush brush = Brushes.Solid(Color.Black); Pen pen = Pens.Solid(Color.OrangeRed, 2F); + Action> drawAction = canvas => + canvas.DrawText(textOptions, text, drawingOptions, brush, pen); using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); - defaultImage.DebugSave( - provider, - "DefaultBackend_DrawText", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using Image webGpuImage = provider.GetImage(); - using WebGPUDrawingBackend backend = new(); - webGpuImage.Configuration.SetDrawingBackend(backend); - webGpuImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); - - webGpuImage.DebugSave( - provider, - "WebGPUBackend_DrawText", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - Assert.True(backend.TestingPrepareCoverageCallCount > 0); - Assert.True(backend.TestingCompositeCoverageCallCount >= backend.TestingPrepareCoverageCallCount); - Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); - Assert.Equal(0, backend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); - - ImageComparer comparer = ImageComparer.TolerantPercentage(4F); - comparer.VerifySimilarity(defaultImage, webGpuImage); + RenderWithDefaultBackend(defaultImage, drawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "DrawText", defaultImage, cpuRegionImage, nativeSurfaceImage); + + Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); + Assert.True(cpuRegionBackend.TestingCompositeCoverageCallCount >= cpuRegionBackend.TestingPrepareCoverageCallCount); + Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(cpuRegionBackend); + + Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); + Assert.True(nativeSurfaceBackend.TestingCompositeCoverageCallCount >= nativeSurfaceBackend.TestingPrepareCoverageCallCount); + Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 4F); } [Theory] @@ -400,72 +415,34 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - - using Image defaultImage = provider.GetImage(); - using (DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + Action> drawAction = canvas => { - defaultCanvas.Fill(clearBrush, clearOptions); - defaultCanvas.FillPath(polygon, brush, drawingOptions); - defaultCanvas.Flush(); - } - - defaultImage.DebugSave( - provider, - "DefaultBackend_FillPath_NativeSurfaceParity", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using WebGPUDrawingBackend backend = new(); - Assert.True( - WebGPUTestNativeSurfaceAllocator.TryCreate( - backend, - defaultImage.Width, - defaultImage.Height, - isSrgb: false, - isPremultipliedAlpha: false, - out NativeSurface nativeSurface, - out nint textureHandle, - out nint textureViewHandle, - out string createError), - createError); - - try - { - Configuration configuration = Configuration.Default.Clone(); - configuration.SetDrawingBackend(backend); - - using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); canvas.Fill(clearBrush, clearOptions); canvas.FillPath(polygon, brush, drawingOptions); - canvas.Flush(); - - Assert.True( - WebGPUTestNativeSurfaceAllocator.TryReadTexture( - backend, - textureHandle, - defaultImage.Width, - defaultImage.Height, - out Image webGpuImage, - out string readError), - readError); + }; - using (webGpuImage) - { - webGpuImage.DebugSave( - provider, - "WebGPUBackend_FillPath_NativeSurfaceParity", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); - comparer.VerifySimilarity(defaultImage, webGpuImage); - } - } - finally - { - WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); - } + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceParity", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } [Theory] @@ -489,75 +466,35 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef RectangularPolygon localPolygon = new(16.25F, 24.5F, 250.5F, 160.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - - using Image defaultImage = provider.GetImage(); - using DrawingCanvas defaultCanvas = new(Configuration.Default, GetFrameRegion(defaultImage)); - defaultCanvas.Fill(clearBrush, clearOptions); - - using (DrawingCanvas defaultRegionCanvas = defaultCanvas.CreateRegion(region)) + Action> drawAction = canvas => { - defaultRegionCanvas.FillPath(localPolygon, brush, drawingOptions); - } - - defaultImage.DebugSave( - provider, - "DefaultBackend_FillPath_NativeSurfaceSubregionParity", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using WebGPUDrawingBackend backend = new(); - Assert.True( - WebGPUTestNativeSurfaceAllocator.TryCreate( - backend, - defaultImage.Width, - defaultImage.Height, - isSrgb: false, - isPremultipliedAlpha: false, - out NativeSurface nativeSurface, - out nint textureHandle, - out nint textureViewHandle, - out string createError), - createError); - - try - { - Configuration configuration = Configuration.Default.Clone(); - configuration.SetDrawingBackend(backend); - - using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(defaultImage.Bounds, nativeSurface)); canvas.Fill(clearBrush, clearOptions); - using (DrawingCanvas regionCanvas = canvas.CreateRegion(region)) - { - regionCanvas.FillPath(localPolygon, brush, drawingOptions); - } - - Assert.True( - WebGPUTestNativeSurfaceAllocator.TryReadTexture( - backend, - textureHandle, - defaultImage.Width, - defaultImage.Height, - out Image webGpuImage, - out string readError), - readError); + using DrawingCanvas regionCanvas = canvas.CreateRegion(region); + regionCanvas.FillPath(localPolygon, brush, drawingOptions); + }; - using (webGpuImage) - { - webGpuImage.DebugSave( - provider, - "WebGPUBackend_FillPath_NativeSurfaceSubregionParity", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageComparer comparer = ImageComparer.TolerantPercentage(0.5F); - comparer.VerifySimilarity(defaultImage, webGpuImage); - } - } - finally - { - WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); - } + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } [Theory] @@ -579,36 +516,42 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi string text = new('A', 200); Brush brush = Brushes.Solid(Color.Black); + Action> drawAction = canvas => + canvas.DrawText(textOptions, text, drawingOptions, brush, pen: null); using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); - defaultImage.DebugSave( - provider, - "DefaultBackend_RepeatedGlyphs", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using Image webGpuImage = provider.GetImage(); - using WebGPUDrawingBackend backend = new(); - webGpuImage.Configuration.SetDrawingBackend(backend); - - webGpuImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); - - webGpuImage.DebugSave( - provider, - "WebGPUBackend_RepeatedGlyphs", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageComparer comparer = ImageComparer.TolerantPercentage(2F); - comparer.VerifySimilarity(defaultImage, webGpuImage); - - Assert.InRange(backend.TestingPrepareCoverageCallCount, 1, 20); - Assert.True(backend.TestingCompositeCoverageCallCount >= backend.TestingPrepareCoverageCallCount); - Assert.Equal(backend.TestingPrepareCoverageCallCount, backend.TestingReleaseCoverageCallCount); - Assert.Equal(0, backend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(backend); - AssertGpuPathWhenRequired(backend); + RenderWithDefaultBackend(defaultImage, drawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "RepeatedGlyphs", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 2F); + + Assert.InRange(cpuRegionBackend.TestingPrepareCoverageCallCount, 1, 20); + Assert.True(cpuRegionBackend.TestingCompositeCoverageCallCount >= cpuRegionBackend.TestingPrepareCoverageCallCount); + Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(cpuRegionBackend); + + Assert.InRange(nativeSurfaceBackend.TestingPrepareCoverageCallCount, 1, 20); + Assert.True(nativeSurfaceBackend.TestingCompositeCoverageCallCount >= nativeSurfaceBackend.TestingPrepareCoverageCallCount); + Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); + Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); } [Theory] @@ -640,37 +583,242 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes string text = new('A', glyphCount); Brush drawBrush = Brushes.Solid(Color.HotPink); Brush clearBrush = Brushes.Solid(Color.White); + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas defaultClearCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + { + defaultClearCanvas.Fill(clearBrush, clearOptions); + defaultClearCanvas.Flush(); + } - using Image image = provider.GetImage(); - using WebGPUDrawingBackend backend = new(); - Configuration configuration = Configuration.Default.Clone(); - configuration.SetDrawingBackend(backend); - - using (DrawingCanvas clearCanvas = new(configuration, GetFrameRegion(image))) + using (DrawingCanvas defaultDrawCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) { - clearCanvas.Fill(clearBrush, clearOptions); - clearCanvas.Flush(); + defaultDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + defaultDrawCanvas.Flush(); } - int computeBatchesBeforeDraw = backend.TestingComputePathBatchCount; + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + Configuration cpuRegionConfiguration = Configuration.Default.Clone(); + cpuRegionConfiguration.SetDrawingBackend(cpuRegionBackend); - using (DrawingCanvas canvas = new(configuration, GetFrameRegion(image))) + using (DrawingCanvas cpuRegionClearCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage))) { - canvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); - canvas.Flush(); + cpuRegionClearCanvas.Fill(clearBrush, clearOptions); + cpuRegionClearCanvas.Flush(); } - AssertGpuPathWhenRequired(backend); - if (!backend.TestingIsGPUReady) + int cpuRegionComputeBatchesBeforeDraw = cpuRegionBackend.TestingComputePathBatchCount; + using (DrawingCanvas cpuRegionDrawCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage))) { - return; + cpuRegionDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + cpuRegionDrawCanvas.Flush(); } - int computeBatchesFromDraw = backend.TestingComputePathBatchCount - computeBatchesBeforeDraw; + int cpuRegionComputeBatchesFromDraw = cpuRegionBackend.TestingComputePathBatchCount - cpuRegionComputeBatchesBeforeDraw; + using WebGPUDrawingBackend nativeSurfaceBackend = new(); Assert.True( - computeBatchesFromDraw > 0, - "Expected repeated-glyph draw batch to execute via tiled compute composition."); + WebGPUTestNativeSurfaceAllocator.TryCreate( + nativeSurfaceBackend, + defaultImage.Width, + defaultImage.Height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration nativeSurfaceConfiguration = Configuration.Default.Clone(); + nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend); + Rectangle targetBounds = defaultImage.Bounds; + + using (DrawingCanvas nativeSurfaceClearCanvas = + new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface))) + { + nativeSurfaceClearCanvas.Fill(clearBrush, clearOptions); + nativeSurfaceClearCanvas.Flush(); + } + + int nativeSurfaceComputeBatchesBeforeDraw = nativeSurfaceBackend.TestingComputePathBatchCount; + using (DrawingCanvas nativeSurfaceDrawCanvas = + new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface))) + { + nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + nativeSurfaceDrawCanvas.Flush(); + } + + int nativeSurfaceComputeBatchesFromDraw = + nativeSurfaceBackend.TestingComputePathBatchCount - nativeSurfaceComputeBatchesBeforeDraw; + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + nativeSurfaceBackend, + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image nativeSurfaceImage, + out string readError), + readError); + + using (nativeSurfaceImage) + { + DebugSaveBackendTriplet(provider, "RepeatedGlyphs_AfterClear", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 2F); + } + + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + + if (cpuRegionBackend.TestingIsGPUReady) + { + Assert.True( + cpuRegionComputeBatchesFromDraw > 0, + "Expected repeated-glyph draw batch to execute via tiled compute composition on the CPURegion pipeline."); + } + + if (nativeSurfaceBackend.TestingIsGPUReady) + { + Assert.True( + nativeSurfaceComputeBatchesFromDraw > 0, + "Expected repeated-glyph draw batch to execute via tiled compute composition on the NativeSurface pipeline."); + } + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + private static void RenderWithDefaultBackend(Image image, Action> drawAction) + where TPixel : unmanaged, IPixel + { + using DrawingCanvas canvas = new(Configuration.Default, GetFrameRegion(image)); + drawAction(canvas); + canvas.Flush(); + } + + private static void RenderWithCpuRegionWebGpuBackend( + Image image, + WebGPUDrawingBackend backend, + Action> drawAction) + where TPixel : unmanaged, IPixel + { + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + using DrawingCanvas canvas = new(configuration, GetFrameRegion(image)); + drawAction(canvas); + canvas.Flush(); + } + + private static Image RenderWithNativeSurfaceWebGpuBackend( + int width, + int height, + WebGPUDrawingBackend backend, + Action> drawAction, + Image? initialImage = null) + where TPixel : unmanaged, IPixel + { + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + backend, + width, + height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + Rectangle targetBounds = new(0, 0, width, height); + + using DrawingCanvas canvas = + new(configuration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface)); + if (initialImage is not null) + { + DrawingOptions copyOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + BlendPercentage = 1F, + ColorBlendingMode = PixelColorBlendingMode.Normal, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + canvas.DrawImage( + initialImage, + initialImage.Bounds, + new RectangleF(0, 0, width, height), + copyOptions); + } + + drawAction(canvas); + canvas.Flush(); + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + backend, + textureHandle, + width, + height, + out Image image, + out string readError), + readError); + + return image; + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + private static void DebugSaveBackendTriplet( + TestImageProvider provider, + string testName, + Image defaultImage, + Image cpuRegionImage, + Image nativeSurfaceImage) + where TPixel : unmanaged, IPixel + { + defaultImage.DebugSave( + provider, + $"{testName}_Default", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + cpuRegionImage.DebugSave( + provider, + $"{testName}_WebGPU_CPURegion", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + nativeSurfaceImage.DebugSave( + provider, + $"{testName}_WebGPU_NativeSurface", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + private static void AssertBackendTripletSimilarity( + Image defaultImage, + Image cpuRegionImage, + Image nativeSurfaceImage, + float defaultTolerancePercent) + where TPixel : unmanaged, IPixel + { + ImageComparer.Exact.VerifySimilarity(cpuRegionImage, nativeSurfaceImage); + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(defaultTolerancePercent); + tolerantComparer.VerifySimilarity(defaultImage, cpuRegionImage); } private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backend) From 8f35205278e2ba82f0571a3bc35ff8c50939cfe5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 27 Feb 2026 14:32:43 +1000 Subject: [PATCH 025/136] Add WebGPU coverage pipeline and WGSL shaders --- .../Shaders/BackdropComputeShader.cs | 114 + .../Shaders/CompositeDestinationBlitShader.cs | Bin 2630 -> 3016 bytes .../Shaders/CompositeDestinationInitShader.cs | Bin 2317 -> 2379 bytes .../Shaders/CoverageFineComputeShader.cs | Bin 0 -> 5561 bytes .../Shaders/CoverageRasterizationShader.cs | Bin 2112 -> 0 bytes .../Shaders/PathCountComputeShader.cs | 256 ++ .../Shaders/PathCountSetupComputeShader.cs | Bin 0 -> 1761 bytes .../Shaders/PathTilingComputeShader.cs | Bin 0 -> 7754 bytes .../Shaders/PathTilingSetupComputeShader.cs | Bin 0 -> 1776 bytes .../Shaders/PreparedCompositeComputeShader.cs | 244 ++ .../Shaders/SegmentAllocComputeShader.cs | Bin 0 -> 2105 bytes .../Shaders/TiledCompositeComputeShader.cs | 362 --- .../WEBGPU_BACKEND_PROCESS.md | 94 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 2084 +++++++++++++++++ .../WebGPUDrawingBackend.TiledComposite.cs | 1076 --------- .../WebGPUDrawingBackend.cs | 1116 +++++++-- .../WebGPUFlushContext.cs | 285 +-- .../WebGPURasterizer.cs | 1064 --------- .../WebGPUTestNativeSurfaceAllocator.cs | 48 + .../Processing/Backends/CompositionCommand.cs | 1 + .../Drawing/DrawTextRepeatedGlyphs.cs | 6 +- .../Backends/WebGPUDrawingBackendTests.cs | 55 +- 22 files changed, 3837 insertions(+), 2968 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/TiledCompositeComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs new file mode 100644 index 000000000..4ae00664d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Null-terminated WGSL compute shader for per-row backdrop prefix propagation. +/// +internal static class BackdropComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct Tile { + backdrop: i32, + segment_count_or_ix: u32, + } + + struct Config { + width_in_tiles: u32, + height_in_tiles: u32, + target_width: u32, + target_height: u32, + base_color: u32, + n_drawobj: u32, + n_path: u32, + n_clip: u32, + bin_data_start: u32, + pathtag_base: u32, + pathdata_base: u32, + drawtag_base: u32, + drawdata_base: u32, + transform_base: u32, + style_base: u32, + lines_size: u32, + binning_size: u32, + tiles_size: u32, + seg_counts_size: u32, + segments_size: u32, + blend_size: u32, + ptcl_size: u32, + } + + @group(0) @binding(0) + var config: Config; + + @group(0) @binding(1) + var tiles: array; + + const WG_SIZE = 64u; + var sh_backdrop: array; + var running_backdrop: i32; + + @compute @workgroup_size(64) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, + ) { + let width_in_tiles = config.width_in_tiles; + let row_index = wg_id.x; + if row_index >= config.height_in_tiles { + return; + } + + if local_id.x == 0u { + running_backdrop = 0; + } + workgroupBarrier(); + + var chunk_start = 0u; + loop { + if chunk_start >= width_in_tiles { + break; + } + + let count = min(WG_SIZE, width_in_tiles - chunk_start); + var backdrop = 0; + if local_id.x < count { + let ix = row_index * width_in_tiles + chunk_start + local_id.x; + backdrop = tiles[ix].backdrop; + } + + sh_backdrop[local_id.x] = backdrop; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x >= (1u << i) { + backdrop += sh_backdrop[local_id.x - (1u << i)]; + } + + workgroupBarrier(); + sh_backdrop[local_id.x] = backdrop; + } + + workgroupBarrier(); + if local_id.x < count { + let ix = row_index * width_in_tiles + chunk_start + local_id.x; + let accumulated = sh_backdrop[local_id.x] + running_backdrop; + tiles[ix].backdrop = accumulated; + if local_id.x + 1u == count { + running_backdrop = accumulated; + } + } + + workgroupBarrier(); + chunk_start += WG_SIZE; + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs index 3f641e9ab83aa7f876e68df46383654c67be88e5..f2c2e4f3bb87f7a0be3c827215ecdeac4c57636e 100644 GIT binary patch delta 402 zcmZXQu};H442IPyYJ`|7AqFZB42cwylrBinz5{~2axYGD;?j$pBd{^@0QVwD47>m! zHXaF*rcq1rV%h%n{kA_xUq>$=gPQ<^Ue&}#t};kj7f>=4_sYmpNlM%(il8!@F(X)_ zxC5h`no(xC0aRsm(uCALsa~9p_jrdP$e(Frl-HOSc8|(JK3u7Mv|%kJq*$DDcr+GlDh351=M7 zy-KI&b1x3#KlLPxZ+-v23sa?4P07ZkB1OV@`<&ffg6$0r&pr=NM!S%p`+#pfcl-;c C^?{@S delta 55 zcmX>heoSP;QO3=ZOy}4qw{i;#8|WG6D4-CUlizU$vK6J4losVp_U2NVyoFl?0MnKb AQUCw| diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs index a557cdbb201ace95950411ad98a50bb11ec0f563..2c869ec9476f266ca3a7e792db164007eeb93a29 100644 GIT binary patch delta 176 zcmeAbIxVyzfiWpFPobbFHMcaUB(oqVGc`pwF{dCSQ6VkAC|994pG#j~U!k}(sW`Q; zG&Qe8Avr&{Aip@XBr_kVEwQ*bwOAoJF;4+#KqAl}bh8wS3lfu4H=krQXXDCAEdiQW uT9lkRS(4+yU^avR diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs new file mode 100644 index 0000000000000000000000000000000000000000..cc123f363ee74ff8ad403b77e8f0e9cef839ebfc GIT binary patch literal 5561 zcmb_gTTkOg6z(&>V!9722ZBj}t||!8c6ZqY3AG4Nw-Q3u#GZs<9org@A#O!~d%rW| zOYArSRrn#c<8%GaWzNjr9{U_8xstPc%^bhSuH}MV@JXyvcWZBtT}WR zFP13SbrMQl(zsh&5ub}R;l3d%q@2(BOkCe{mAGFNf0EJ6y;QL;(v%)QasP*ig7kO` z+Vxs1AJ3Ca3zpt@f2k8Z;`T8%;qA} zDU*?o8IRc2!Twh;JqiUom=+72DM7wL+g_y()Ivo(WU1y_`pge`ngSZj_EkpWv(*bv zpmyt@Eru&m@)4GmOiJ(|j>4QxaxHG}z#fQCcud)eA-^@h#OS)! ztQ}L#;Q>zwg{g|?>`xJfv7vFFW@*?1_#~GuKaNt(Zq6@Gz3=B=ZqC>V8-B=+yS|^D zo_{;LDG4fyRIAL_?Ak)ftglu$e2#(D;D|j6e}6PR*dO;RJ|Z!nhigCQo(N{*h=H-@ zd#;TyRNu%@wDeApDS?WUBPN%5NoNsr&yTZ6d$IE5;)rF-RI3_FoG-cJ^R%T)b2Ss% zdy;{^Z%HD3#pioLRnzqy*24@rlabf9#PBbD&)4xAW0gEw#o3pFa-@c%B4^i6{ z!qW&%0Rd0AzIR}*7f+|DD3X*4#bRN@&6+jE#G{D3I6yK?;yCQH1(LHabEZU^g%Fi! zHJWCTuVozd3rwk=*@|`Jaj#Wb$QN)3ULv@LukKcqHlfg!sooSWgUcyx?=@+WiOeA_ z5_E&?828btVf6P7`eN3$kJOQUxAG4Br4Db}RPtQmP4t~AE2yDrXQxXIc~wLRs_Iy= z9VVOVNJ&9WYn!DutK4nDcGu02*QBoP+iuQw5mz_wW(Y*6d7Ehn7U&^`g_LgQd;m@? zob$xV`)uG2`fTV9TJb4;$%i#DJT{8mm)Qbx35s@EK3ugzP8kS(M&R8+EAdD#U4$cY z+RHs9e3?KRY-D?-0jn}gCQaTsR&|eUFALegL|!`Z752WG2_uNo3otf8cNaEvi}mUj zWMjd;!(j1t;nyY}GO^|vzKRDE8L7g}Ss=`dVMmlp;1+Xk?80p`aaBhT5KX1~&|SI@ z>r0n((nV2zs9MFt8tY2R1^g(zF`n%bv&~*$jsy|AAG%SmMR1Ck_0_O%rk_;$_+nj| zkZiih8K39@;KUTx3;qNJG5Pfxda8J5f+`%izaachKsf-%ecZ7nK*NBb9*V>v|H}9E z4w?q~a7SU{B&ws?kYCL%+WimtZf8c=n95n?*iNyaYO=kCx*2RLi>096p|EZ2LT{9A zOU_!=tGF~BZN8ypmCJ5N$9f|h2}KHWVe6`e1b*oV$R?@70qH(E<2J+i7X^+@4c_ZW zy{1FYOoVBBcqjR9?Ov?4_Xb$tM1$@YW`$qfV5-u0E>6N3bo0kY33PnwHEa5q=m$%u9M(7}c7hO1}%@ zO=Kk4PTC;;m&Cb6KU&_DHti$vRq0n~RNFN(#w?Har4u&b3mSxIICp3mE|UN=!uIO4 zi^^yaUgk-ATIj{?ww;5&1Xo zDyayCuhH-9_(~fb(_R=d7ESQw{?~MdqMXQu2)zmR*+tAukvF>pMYr(UWMg@JfOsw) zh&ap-)6|WImoc}CK&D+s<1AqwwKaN#^gRo^Vc5DJaV42Q3a77+iF1R|X zz*#z)Vej9U6!{8;Z5kaJL!cDLfY&%}5RToD_l+?l94?I=s}^O00y}hy1Sp0HlNk7E z*AR(ax6(ZSR3$i$`?6on8@i3TC)p&#iDPl_c!e{>KX8V4o$zQx=ZIr-jL1&x-5e(# IZ#{4Q2Q&++i~s-t literal 0 HcmV?d00001 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageRasterizationShader.cs deleted file mode 100644 index d3b6e11132678ed481fa1169db30eb1d2a004255..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2112 zcmcIl%Wm5^6z$qyap9GW`ZY-xLE;#lc4mSC0UFfFbWs!mO}f=^S?CvB;;;>qZLPE{Oc>mA5iAzVnp2*urjW$ znQqr%_5Nye4W?wiv-2RfZ9u@)9yL`3&5T7YK6_*ewoX(z&=<)?1x)6U0vl!w21e9l zF=(>VNF5(cY1GUOsg_JEV6Vkv|*%C0{;i6x>a8+fvMwGD+ptQP$MhR(6A8+7t zgrc!wnpvSFVI>w`XOAl3pnTMlReswF4!k1tG^R-U-GOtcK)RARqc0<%XDDFz^Zte~an{`vy7>Sb+gxsqK@|`WUa=Vfc5)QFP`19rsHPeN+ zx-FUYULjWz39oM=%{(bjV%CueCbk$|f_e^rORjQH*^j7;BC5I2$Ze1(Y;jdLqQaRK zHKzKVplq*1Za_OywMHOW6^|t1DQH^o?kSPwKM?2WKtIFAZ7VY0!GrH8^{uShO~d5M z`({ms%n>f*gnhzzJPyG8yRq7F3;&|F`1sb@##x9a^}GIg=})T_GHa9(-U}1bB)*-) z9{Gz^aeA_zrixE=L&QwVB`#DqtqTczbMFsm_OfH1Ex#&w5(-XWw*C>UJ=359-t$4& zMQY|j#E5wZ7%q>96Q3z8=gTSlF<(xG$r`QG@=)Wr_0;26(Ct>!5lKynVPWnMw@(&G z445XtzEb(nD{W^1)8MCvt`Fh65{D}yOQVZ1_k#W!9w#J$A4*)EgF8L>8Lc}B4Qa@< zX6=fw#!*e-be$yyy4k1gY(~;f!T(N +/// Null-terminated WGSL compute shader for path segment counting. +/// +internal static class PathCountComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + // Path count stage (derived from Vello path_count.wgsl). + + const STAGE_BINNING: u32 = 0x1u; + const STAGE_TILE_ALLOC: u32 = 0x2u; + const STAGE_FLATTEN: u32 = 0x4u; + const STAGE_PATH_COUNT: u32 = 0x8u; + const STAGE_COARSE: u32 = 0x10u; + + struct BumpAllocators { + failed: atomic, + binning: atomic, + ptcl: atomic, + tile: atomic, + seg_counts: atomic, + segments: atomic, + blend: atomic, + lines: atomic, + } + + struct Config { + width_in_tiles: u32, + height_in_tiles: u32, + target_width: u32, + target_height: u32, + base_color: u32, + n_drawobj: u32, + n_path: u32, + n_clip: u32, + bin_data_start: u32, + pathtag_base: u32, + pathdata_base: u32, + drawtag_base: u32, + drawdata_base: u32, + transform_base: u32, + style_base: u32, + lines_size: u32, + binning_size: u32, + tiles_size: u32, + seg_counts_size: u32, + segments_size: u32, + blend_size: u32, + ptcl_size: u32, + } + + const TILE_WIDTH = 16u; + const TILE_HEIGHT = 16u; + const TILE_SCALE = 0.0625; + + struct LineSoup { + path_ix: u32, + p0: vec2, + p1: vec2, + } + + struct SegmentCount { + line_ix: u32, + counts: u32, + } + + struct Path { + bbox: vec4, + tiles: u32, + } + + struct Tile { + backdrop: i32, + segment_count_or_ix: u32, + } + + // TODO: this is cut'n'pasted from path_coarse. + struct AtomicTile { + backdrop: atomic, + segment_count_or_ix: atomic, + } + + @group(0) @binding(0) + var config: Config; + + @group(0) @binding(1) + var bump: BumpAllocators; + + @group(0) @binding(2) + var lines: array; + + @group(0) @binding(3) + var paths: array; + + @group(0) @binding(4) + var tile: array; + + @group(0) @binding(5) + var seg_counts: array; + + fn span(a: f32, b: f32) -> u32 { + return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); + } + + const ONE_MINUS_ULP: f32 = 0.99999994; + const ROBUST_EPSILON: f32 = 2e-7; + + @compute @workgroup_size(256) + fn cs_main( + @builtin(global_invocation_id) global_id: vec3, + ) { + let n_lines = atomicLoad(&bump.lines); + var count = 0u; + if global_id.x < n_lines { + let line = lines[global_id.x]; + let is_down = line.p1.y >= line.p0.y; + let xy0 = select(line.p1, line.p0, is_down); + let xy1 = select(line.p0, line.p1, is_down); + let s0 = xy0 * TILE_SCALE; + let s1 = xy1 * TILE_SCALE; + let count_x = span(s0.x, s1.x) - 1u; + count = count_x + span(s0.y, s1.y); + let line_ix = global_id.x; + + let dx = abs(s1.x - s0.x); + let dy = s1.y - s0.y; + if dx + dy == 0.0 { + return; + } + if dy == 0.0 && floor(s0.y) == s0.y { + return; + } + let idxdy = 1.0 / (dx + dy); + var a = dx * idxdy; + let is_positive_slope = s1.x >= s0.x; + let x_sign = select(-1.0, 1.0, is_positive_slope); + let xt0 = floor(s0.x * x_sign); + let c = s0.x * x_sign - xt0; + let y0 = floor(s0.y); + let ytop = select(y0 + 1.0, ceil(s0.y), s0.y == s1.y); + let b = min((dy * c + dx * (ytop - s0.y)) * idxdy, ONE_MINUS_ULP); + let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); + if robust_err != 0.0 { + a -= ROBUST_EPSILON * sign(robust_err); + } + let x0 = xt0 * x_sign + select(-1.0, 0.0, is_positive_slope); + + let path = paths[line.path_ix]; + let bbox = vec4(path.bbox); + let xmin = min(s0.x, s1.x); + let stride = bbox.z - bbox.x; + if s0.y >= f32(bbox.w) || s1.y <= f32(bbox.y) || xmin >= f32(bbox.z) || stride == 0 { + return; + } + var imin = 0u; + if s0.y < f32(bbox.y) { + var iminf = round((f32(bbox.y) - y0 + b - a) / (1.0 - a)) - 1.0; + if y0 + iminf - floor(a * iminf + b) < f32(bbox.y) { + iminf += 1.0; + } + imin = u32(iminf); + } + var imax = count; + if s1.y > f32(bbox.w) { + var imaxf = round((f32(bbox.w) - y0 + b - a) / (1.0 - a)) - 1.0; + if y0 + imaxf - floor(a * imaxf + b) < f32(bbox.w) { + imaxf += 1.0; + } + imax = u32(imaxf); + } + let delta = select(1, -1, is_down); + var ymin = 0; + var ymax = 0; + if max(s0.x, s1.x) <= f32(bbox.x) { + ymin = i32(ceil(s0.y)); + ymax = i32(ceil(s1.y)); + imax = imin; + } else { + let fudge = select(1.0, 0.0, is_positive_slope); + if xmin < f32(bbox.x) { + var f = round((x_sign * (f32(bbox.x) - x0) - b + fudge) / a); + if (x0 + x_sign * floor(a * f + b) < f32(bbox.x)) == is_positive_slope { + f += 1.0; + } + let ynext = i32(y0 + f - floor(a * f + b) + 1.0); + if is_positive_slope { + if u32(f) > imin { + ymin = i32(y0 + select(1.0, 0.0, y0 == s0.y)); + ymax = ynext; + imin = u32(f); + } + } else { + if u32(f) < imax { + ymin = ynext; + ymax = i32(ceil(s1.y)); + imax = u32(f); + } + } + } + if max(s0.x, s1.x) > f32(bbox.z) { + var f = round((x_sign * (f32(bbox.z) - x0) - b + fudge) / a); + if (x0 + x_sign * floor(a * f + b) < f32(bbox.z)) == is_positive_slope { + f += 1.0; + } + if is_positive_slope { + imax = min(imax, u32(f)); + } else { + imin = max(imin, u32(f)); + } + } + } + imax = max(imin, imax); + ymin = max(ymin, bbox.y); + ymax = min(ymax, bbox.w); + for (var y = ymin; y < ymax; y++) { + let base = i32(path.tiles) + (y - bbox.y) * stride; + atomicAdd(&tile[base].backdrop, delta); + } + var last_z = floor(a * (f32(imin) - 1.0) + b); + let seg_base = atomicAdd(&bump.seg_counts, imax - imin); + for (var i = imin; i < imax; i++) { + let subix = i; + let zf = a * f32(subix) + b; + let z = floor(zf); + let y = i32(y0 + f32(subix) - z); + let x = i32(x0 + x_sign * z); + let base = i32(path.tiles) + (y - bbox.y) * stride - bbox.x; + let top_edge = select(last_z == z, y0 == s0.y, subix == 0u); + if top_edge && x + 1 < bbox.z { + let x_bump = max(x + 1, bbox.x); + atomicAdd(&tile[base + x_bump].backdrop, delta); + } + let seg_within_slice = atomicAdd(&tile[base + x].segment_count_or_ix, 1u); + let counts = (seg_within_slice << 16u) | subix; + let seg_count = SegmentCount(line_ix, counts); + let seg_ix = seg_base + i - imin; + if seg_ix < config.seg_counts_size { + seg_counts[seg_ix] = seg_count; + } + last_z = z; + } + } + } + + """u8 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs new file mode 100644 index 0000000000000000000000000000000000000000..4ede0c4ff00644a7dc3177ee61d78e9cdb8958cc GIT binary patch literal 1761 zcma)6ZExBz5Z>qfid#QSiPna;dr?Ix;$@5>MNlBN8EIf0N; zK=TK1iKPNJEze%#-V&>k;gfgYfwL|D1CPH;>p|=_|R2IiXE}Qw+ zFpHuy&1B34(HO&h-}itfahhocYEDc<(?mi_^!$t_na~h&^snh06w_I1Qb5vL$;t)N z2@-+QL~{x>B1*w6+INk)Z;ZNy0}L7Yt-luFNhN>65H@C#kuVX_5++N{9v)G`kah5? z;3kJZ0;b7RWL-y$sZs7d+BD>xT91px+(DuAhcZ&Wkxl0vN&y{2$X1SODHN*WDPJSJYr!T z@Ga)Fff+YXO9BI$a3QeMjv!J^qa)869UOU9Y*rLT9S6ls=wpycgmZ9&BI1G_vR}Q? zY#>4|8P$1P??_g((Q~f)Ypsfx<-wPiotKK=)=VZ@YM(pMLd8Q=9Peczo``HHtRAj{ zQ!vvLEF{;g2@|YseW${e4%owb@gJ-v0F!+kUg=OFF2?1)ANYgcJ)DutFJG&j*V=5E z)*_LA^3(#wUzlC-J{3S!5ECwJXGdW^UH7AxkkI~Ml5~qkb>KY?WVVAheY%O+U6;oU zZe?l=WCbizY>#ckH01q(M#Ugh9OnNU-( zEbtz}B?ePnJbNH7JCN18k`T9Lc9nlUuFIBYlL%M#dUM}l6OC{M`za9(vxb}b7KWSR LKi|gJ*2?+^G12Yk literal 0 HcmV?d00001 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs new file mode 100644 index 0000000000000000000000000000000000000000..54ce2c083ec0e6abd262e9b7e7c3b390f9f47480 GIT binary patch literal 7754 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 E3rq!cH2?qr literal 0 HcmV?d00001 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs new file mode 100644 index 0000000000000000000000000000000000000000..2f61c64a3f82521cf34f27d1324fa1d672057202 GIT binary patch literal 1776 zcma)6ZExBz5Z>qfid#QSiPna;dr?Ix;$@5>MNlBN8EIf0Ne zK=TK1iKPNJEze%#-V&>k;gfgYfwL|C=6QR1c&|3`}DvM(xm(BcZ zm_^ZEXcD)xk9(cW=?Rlf_7JGRB``v4g$Gzbe`1t_v zwmtq4bVomj<1O~;0Jb}7-}${Q?&2JQWvEKajB4m)aoR=|DACxOP}HQz8emFz#KJn@ zTg+(#Gj5)i1O_zWLSSTk6wUX}-6UUyz9ep@q{WT}1bKnoQQQE|MNg?J{ip|E&1VtngC4pb$Fvgg}4}(`+ndLe)n)jF28)Oc3x|< zXpgyl{WQXT?59LD%ua6R PD;aKz|9mxHTPy1?R6g_+ literal 0 HcmV?d00001 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs new file mode 100644 index 000000000..da21ec2bd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs @@ -0,0 +1,244 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Null-terminated WGSL compute shader for prepared composition batches. +/// +internal static class PreparedCompositeComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct Params { + destination_x: u32, + destination_y: u32, + destination_width: u32, + destination_height: u32, + coverage_offset_x: u32, + coverage_offset_y: u32, + target_width: u32, + brush_type: u32, + brush_origin_x: u32, + brush_origin_y: u32, + brush_region_x: u32, + brush_region_y: u32, + brush_region_width: u32, + brush_region_height: u32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + solid_r: u32, + solid_g: u32, + solid_b: u32, + solid_a: u32, + }; + + struct DispatchConfig { + command_count: u32, + target_width: u32, + target_height: u32, + pad0: u32, + }; + + @group(0) @binding(0) var coverage_texture: texture_2d; + @group(0) @binding(1) var source_texture: texture_2d; + @group(0) @binding(2) var destination_pixels: array>; + @group(0) @binding(3) var commands: array; + @group(0) @binding(4) var dispatch_config: DispatchConfig; + + fn u32_to_f32(bits: u32) -> f32 { + return bitcast(bits); + } + + fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { + if (alpha <= 0.0) { + return vec3(0.0); + } + + return rgb / alpha; + } + + fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { + switch mode { + case 1u: { + return backdrop * source; + } + case 2u: { + return backdrop + source; + } + case 3u: { + return backdrop - source; + } + case 4u: { + return 1.0 - ((1.0 - backdrop) * (1.0 - source)); + } + case 5u: { + return min(backdrop, source); + } + case 6u: { + return max(backdrop, source); + } + case 7u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + backdrop >= vec3(0.5)); + } + case 8u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + source >= vec3(0.5)); + } + default: { + return source; + } + } + } + + fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { + let destination_alpha = destination_premul.a; + let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); + let source_alpha = source.a; + let source_rgb = source.rgb; + let source_premul = source_rgb * source_alpha; + let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); + let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); + let shared_alpha = source_alpha * destination_alpha; + + switch alpha_mode { + case 1u: { + return vec4(source_premul, source_alpha); + } + case 2u: { + let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); + return vec4(premul, destination_alpha); + } + case 3u: { + let alpha = source_alpha * destination_alpha; + return vec4(source_premul * destination_alpha, alpha); + } + case 4u: { + let alpha = source_alpha * (1.0 - destination_alpha); + return vec4(source_premul * (1.0 - destination_alpha), alpha); + } + case 5u: { + return destination_premul; + } + case 6u: { + let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); + return vec4(premul, source_alpha); + } + case 7u: { + let alpha = destination_alpha + source_alpha - shared_alpha; + let premul = + (source_rgb * (source_alpha - shared_alpha)) + + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (reverse_blend * shared_alpha); + return vec4(premul, alpha); + } + case 8u: { + let alpha = destination_alpha * source_alpha; + return vec4(destination_premul.rgb * source_alpha, alpha); + } + case 9u: { + let alpha = destination_alpha * (1.0 - source_alpha); + return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); + } + case 10u: { + return vec4(0.0, 0.0, 0.0, 0.0); + } + case 11u: { + let source_term = source_premul * (1.0 - destination_alpha); + let destination_term = destination_premul.rgb * (1.0 - source_alpha); + let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); + return vec4(source_term + destination_term, alpha); + } + default: { + let alpha = source_alpha + destination_alpha - shared_alpha; + let premul = + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (source_rgb * (source_alpha - shared_alpha)) + + (forward_blend * shared_alpha); + return vec4(premul, alpha); + } + } + } + + fn positive_mod(value: i32, divisor: i32) -> i32 { + let m = value % divisor; + return select(m + divisor, m, m >= 0); + } + + @compute @workgroup_size(8, 8, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + if (global_id.x >= dispatch_config.target_width || global_id.y >= dispatch_config.target_height) { + return; + } + + let dest_x = i32(global_id.x); + let dest_y = i32(global_id.y); + let dest_index = (global_id.y * dispatch_config.target_width) + global_id.x; + var destination = destination_pixels[dest_index]; + + var command_index: u32 = 0u; + loop { + if (command_index >= dispatch_config.command_count) { + break; + } + + let command = commands[command_index]; + let command_min_x = i32(command.destination_x); + let command_min_y = i32(command.destination_y); + let command_max_x = command_min_x + i32(command.destination_width); + let command_max_y = command_min_y + i32(command.destination_height); + if (dest_x >= command_min_x && dest_x < command_max_x && dest_y >= command_min_y && dest_y < command_max_y) { + let local_x = dest_x - command_min_x; + let local_y = dest_y - command_min_y; + let coverage_x = i32(command.coverage_offset_x) + local_x; + let coverage_y = i32(command.coverage_offset_y) + local_y; + let coverage_value = textureLoad(coverage_texture, vec2(coverage_x, coverage_y), 0).x; + if (coverage_value > 0.0) { + let blend_percentage = u32_to_f32(command.blend_percentage); + let effective_coverage = coverage_value * blend_percentage; + + var brush = vec4( + u32_to_f32(command.solid_r), + u32_to_f32(command.solid_g), + u32_to_f32(command.solid_b), + u32_to_f32(command.solid_a)); + + if (command.brush_type == 1u) { + let origin_x = i32(command.brush_origin_x); + let origin_y = i32(command.brush_origin_y); + let region_x = i32(command.brush_region_x); + let region_y = i32(command.brush_region_y); + let region_w = i32(command.brush_region_width); + let region_h = i32(command.brush_region_height); + let src_x = positive_mod(dest_x - origin_x, region_w) + region_x; + let src_y = positive_mod(dest_y - origin_y, region_h) + region_y; + brush = textureLoad(source_texture, vec2(src_x, src_y), 0); + } + + let source = vec4(brush.rgb, brush.a * effective_coverage); + destination = compose_pixel(destination, source, command.color_blend_mode, command.alpha_composition_mode); + } + } + + command_index = command_index + 1u; + } + + destination_pixels[dest_index] = destination; + } + """u8, + 0 + ]; + + /// + /// Gets the null-terminated UTF-8 WGSL source bytes. + /// + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs new file mode 100644 index 0000000000000000000000000000000000000000..585bd2a8eb1e32e9a056879d25b56d3cb8a37b97 GIT binary patch literal 2105 zcmbVNTW{Jh6y7tx;;0W3NNs58HdPc5mvvnwM61HIhpLJk$ADGGj%%RT zg;mQ#aPW7&b3dnEhk;6$n$PAIs3=XhoSZ?${^vfyvmMS5R9WS|&>pqL&^2 zHOhoIvP>sj5{owcyc`dKswB-U12ZSiqN&nAgiw@Nkk=s369`IG5@?W)s&0+mVW}*J7>GCh*h7c8pftlKT-V9r7oilM?k>!)CAC5KWSonQ?K;8FzDa zXIf=x^|%II%u|ec!gG9Mn z$+G1F*xAq7cqeCB9-i5i=zikUZQ@LF8~{lNQXl5#dS>{Z$V0!$x1NyQN2MQpd_%*3 zv1;SP)DJh>**LAE!!&Jg&j|U#!gqqo=;ZYA;GlFbh82 zxINmXMDSFO9oLF{TA(`Zl{Z0ICND^xgl0C=a+_tN6dlCr_Wg&NeFpDVmfQz#l=U`M zsoNA%NaN|Yk!bo#3C`2(|1EXXBHu} -/// WGSL compute shader for tiled brush composition over prepared path coverage. -///
-/// -/// The shader resolves tile-local command ranges, samples brush/source data, applies color blending -/// and Porter-Duff alpha composition, and writes updated destination pixels into storage. -/// -internal static class TiledCompositeComputeShader -{ - /// - /// Gets the UTF-8 WGSL source bytes used by the tiled composite compute pipeline. - /// - /// - /// - /// The literal intentionally includes a trailing U+0000 null terminator before the suffix. - /// - /// - /// Native WebGPU shader creation expects WGSL as a null-terminated byte pointer. The explicit - /// terminator keeps shader bytes as a compile-time constant and avoids runtime append/copy overhead. - /// - /// - public static ReadOnlySpan Code => - """ - struct CompositeCommand { - source_offset_x: i32, - source_offset_y: i32, - destination_x: i32, - destination_y: i32, - destination_width: i32, - destination_height: i32, - blend_percentage: f32, - color_blending_mode: i32, - alpha_composition_mode: i32, - brush_data_index: i32, - _pad0: i32, - _pad1: i32, - }; - - struct TileRange { - start_index: u32, - count: u32, - }; - - struct BrushData { - source_region_x: i32, - source_region_y: i32, - source_region_width: i32, - source_region_height: i32, - brush_origin_x: i32, - brush_origin_y: i32, - source_layer: i32, - _pad0: i32, - }; - - struct TiledCompositeParams { - destination_width: i32, - destination_height: i32, - tiles_x: i32, - tile_size: i32, - }; - - @group(0) @binding(0) - var coverage: texture_2d; - - @group(0) @binding(1) - var commands: array; - - @group(0) @binding(2) - var tile_ranges: array; - - @group(0) @binding(3) - var tile_command_indices: array; - - @group(0) @binding(4) - var brushes: array; - - @group(0) @binding(5) - var source_layers: texture_2d_array; - - @group(0) @binding(6) - var destination_pixels: array>; - - @group(0) @binding(7) - var params: TiledCompositeParams; - - fn overlay_value(backdrop: f32, source: f32) -> f32 { - if (backdrop <= 0.5) { - return 2.0 * backdrop * source; - } - - return 1.0 - (2.0 * (1.0 - source) * (1.0 - backdrop)); - } - - fn blend_color(backdrop: vec3, source: vec3, color_mode: i32) -> vec3 { - switch color_mode { - case 0 { - return source; - } - - case 1 { - return backdrop * source; - } - - case 2 { - return min(vec3(1.0), backdrop + source); - } - - case 3 { - return max(vec3(0.0), backdrop - source); - } - - case 4 { - return vec3(1.0) - ((vec3(1.0) - backdrop) * (vec3(1.0) - source)); - } - - case 5 { - return min(backdrop, source); - } - - case 6 { - return max(backdrop, source); - } - - case 7 { - return vec3( - overlay_value(backdrop.r, source.r), - overlay_value(backdrop.g, source.g), - overlay_value(backdrop.b, source.b)); - } - - case 8 { - return vec3( - overlay_value(source.r, backdrop.r), - overlay_value(source.g, backdrop.g), - overlay_value(source.b, backdrop.b)); - } - - default { - return source; - } - } - } - - fn unpremultiply(premultiplied_rgb: vec3, alpha: f32) -> vec4 { - let clamped_alpha = clamp(alpha, 0.0, 1.0); - if (clamped_alpha <= 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let color = clamp( - premultiplied_rgb / clamped_alpha, - vec3(0.0), - vec3(1.0)); - return vec4(color, clamped_alpha); - } - - fn compose_over(destination: vec4, source: vec4, blend: vec3) -> vec4 { - let source_weight = source.a; - let destination_weight = destination.a; - let blend_weight = source_weight * destination_weight; - let destination_only_weight = destination_weight - blend_weight; - let source_only_weight = source_weight - blend_weight; - let alpha = destination_only_weight + source_weight; - let premultiplied_color = - (destination.rgb * destination_only_weight) + - (source.rgb * source_only_weight) + - (blend * blend_weight); - return unpremultiply(premultiplied_color, alpha); - } - - fn compose_atop(destination: vec4, source: vec4, blend: vec3) -> vec4 { - let source_weight = source.a; - let destination_weight = destination.a; - let blend_weight = source_weight * destination_weight; - let destination_only_weight = destination_weight - blend_weight; - let premultiplied_color = - (destination.rgb * destination_only_weight) + - (blend * blend_weight); - return unpremultiply(premultiplied_color, destination_weight); - } - - fn compose_in(destination: vec4, source: vec4) -> vec4 { - let alpha = destination.a * source.a; - return unpremultiply(source.rgb * alpha, alpha); - } - - fn compose_out(destination: vec4, source: vec4) -> vec4 { - let alpha = (1.0 - destination.a) * source.a; - return unpremultiply(source.rgb * alpha, alpha); - } - - fn compose_xor(destination: vec4, source: vec4) -> vec4 { - let source_weight = 1.0 - destination.a; - let destination_weight = 1.0 - source.a; - let alpha = (source.a * source_weight) + (destination.a * destination_weight); - let premultiplied_color = - (source.a * source.rgb * source_weight) + - (destination.a * destination.rgb * destination_weight); - return unpremultiply(premultiplied_color, alpha); - } - - fn compose_pixel( - destination: vec4, - source: vec4, - blend_percentage: f32, - color_mode: i32, - alpha_mode: i32) -> vec4 { - let source_alpha = clamp(source.a * blend_percentage, 0.0, 1.0); - let source_color = clamp(source.rgb, vec3(0.0), vec3(1.0)); - let source_with_opacity = vec4(source_color, source_alpha); - let destination_color = clamp(destination.rgb, vec3(0.0), vec3(1.0)); - let destination_alpha = clamp(destination.a, 0.0, 1.0); - let destination_pixel = vec4(destination_color, destination_alpha); - - switch alpha_mode { - case 0 { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_over(destination_pixel, source_with_opacity, blend); - } - - case 1 { - return source_with_opacity; - } - - case 2 { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_atop(destination_pixel, source_with_opacity, blend); - } - - case 3 { - return compose_in(destination_pixel, source_with_opacity); - } - - case 4 { - return compose_out(destination_pixel, source_with_opacity); - } - - case 5 { - return destination_pixel; - } - - case 6 { - let blend = blend_color(source_color, destination_color, color_mode); - return compose_atop(source_with_opacity, destination_pixel, blend); - } - - case 7 { - let blend = blend_color(source_color, destination_color, color_mode); - return compose_over(source_with_opacity, destination_pixel, blend); - } - - case 8 { - return compose_in(source_with_opacity, destination_pixel); - } - - case 9 { - return compose_out(source_with_opacity, destination_pixel); - } - - case 10 { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - case 11 { - return compose_xor(destination_pixel, source_with_opacity); - } - - default { - let blend = blend_color(destination_color, source_color, color_mode); - return compose_over(destination_pixel, source_with_opacity, blend); - } - } - } - - fn positive_mod(value: i32, divisor: i32) -> i32 { - return ((value % divisor) + divisor) % divisor; - } - - fn sample_brush(brush_data: BrushData, destination_x: i32, destination_y: i32) -> vec4 { - if (brush_data.source_region_width <= 0 || brush_data.source_region_height <= 0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let source_x = positive_mod( - destination_x - brush_data.brush_origin_x, - brush_data.source_region_width) + brush_data.source_region_x; - let source_y = positive_mod( - destination_y - brush_data.brush_origin_y, - brush_data.source_region_height) + brush_data.source_region_y; - return textureLoad(source_layers, vec2(source_x, source_y), brush_data.source_layer, 0); - } - - @compute @workgroup_size(8, 8, 1) - fn cs_main( - @builtin(workgroup_id) workgroup_id: vec3, - @builtin(local_invocation_id) local_id: vec3) - { - let tile_x = i32(workgroup_id.x); - let tile_y = i32(workgroup_id.y); - if (tile_x < 0 || tile_x >= params.tiles_x || tile_y < 0) { - return; - } - - let pixel_x = tile_x * params.tile_size + i32(local_id.x); - let pixel_y = tile_y * params.tile_size + i32(local_id.y); - if (pixel_x < 0 || - pixel_y < 0 || - pixel_x >= params.destination_width || - pixel_y >= params.destination_height) - { - return; - } - - let destination_index = (pixel_y * params.destination_width) + pixel_x; - var destination = destination_pixels[destination_index]; - - let tile_index = (tile_y * params.tiles_x) + tile_x; - let tile_range = tile_ranges[tile_index]; - let tile_end = tile_range.start_index + tile_range.count; - var tile_cursor = tile_range.start_index; - loop { - if (tile_cursor >= tile_end) { - break; - } - - let command_index = tile_command_indices[tile_cursor]; - let command = commands[command_index]; - if (pixel_x >= command.destination_x && - pixel_y >= command.destination_y && - pixel_x < (command.destination_x + command.destination_width) && - pixel_y < (command.destination_y + command.destination_height)) - { - let local_x = pixel_x - command.destination_x; - let local_y = pixel_y - command.destination_y; - let coverage_source = vec2( - command.source_offset_x + local_x, - command.source_offset_y + local_y); - let coverage_value = textureLoad(coverage, coverage_source, 0).r; - if (coverage_value > 0.0) { - let brush = sample_brush(brushes[command.brush_data_index], pixel_x, pixel_y); - let source = vec4(brush.rgb, brush.a * coverage_value); - destination = compose_pixel( - destination, - source, - command.blend_percentage, - command.color_blending_mode, - command.alpha_composition_mode); - } - } - - tile_cursor = tile_cursor + 1u; - } - - destination_pixels[destination_index] = destination; - } - """u8; -} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 4dcf0dd93..b8e96fb0a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -1,61 +1,81 @@ # WebGPU Backend Process -This document describes the runtime flow used by `WebGPUDrawingBackend` when flushing a `CompositionScene`. +This document describes the current runtime flow used by `WebGPUDrawingBackend` when flushing a `CompositionScene`. ## End-to-End Flow ```text DrawingCanvasBatcher.Flush() -> IDrawingBackend.FlushCompositions(scene) - -> CompositionScenePlanner.CreatePreparedBatches(scene.Commands) - -> foreach prepared batch - -> WebGPUDrawingBackend.FlushPreparedBatch(batch) - -> validate brush support + pixel format support - -> acquire WebGPUFlushContext - -> shared session context when scene uses GPU-only brushes - -> standalone context otherwise - -> prepare/reuse GPU coverage texture for batch definition - -> composite commands (tiled compute path) - -> build tile ranges + tile command indices - -> build brush/source layer payloads - -> upload command/brush/tile buffers - -> dispatch compute workgroups - -> optional destination blit to target texture - -> finalize - -> submit GPU commands - -> readback to CPU region when target requires readback - -> on any GPU failure path: execute batch through DefaultDrawingBackend + -> capability checks first + -> TryGetCompositeTextureFormat + -> AreAllCompositionBrushesSupported + -> if unsupported: scene-scoped fallback (DefaultDrawingBackend) + -> CompositionScenePlanner.CreatePreparedBatches(commands, targetBounds) + -> clip each command to target bounds + -> group contiguous commands by DefinitionKey + -> keep prepared destination/source offsets + -> acquire one WebGPUFlushContext for the scene + -> for each prepared batch + -> ensure command encoder (single encoder reused for the scene) + -> initialize destination storage buffer once per flush (premultiplied vec4) + -> source = target view when sampleable + -> else copy target region into transient composition texture, then sample that + -> run CompositeDestinationInitShader compute pass + -> build coverage texture from prepared geometry + -> flatten prepared path geometry + -> upload line/path/tile/segment buffers + -> run compute sequence: + 1) PathCountSetup + 2) PathCount + 3) Backdrop + 4) SegmentAlloc + 5) PathTilingSetup + 6) PathTiling + 7) CoverageFine + -> composite commands into destination storage (PreparedCompositeComputeShader) + -> solid brush uses Color.ToScaledVector4() + -> image brush samples Image texture directly + -> blit destination storage back to target (CompositeDestinationBlitShader) + -> render pass uses LoadOp.Load + StoreOp.Store + -> scissor limits writes to destination bounds + -> finalize once + -> finish encoder + -> single queue submit for the flush context + -> optional readback for CPU-region targets + -> on any GPU failure path: scene-scoped fallback (DefaultDrawingBackend) ``` ## Context and Resource Lifetime -- `WebGPUFlushContext` owns per-flush transient resources: - - command encoder - - bind groups - - transient buffers and textures - - optional readback buffer mapping sequence -- shared flush sessions are keyed by scene flush id: - - destination initialization is performed once - - destination storage buffer is reused across all session batches - - session is closed on final batch or on failure +- `WebGPUFlushContext` is created once per `FlushCompositions` execution. +- The same command encoder is reused across all batch passes in that flush. +- Destination storage (`CompositeDestinationPixelsBuffer`) is initialized once and reused across batches in the same flush. +- Transient textures/buffers/bind-groups are tracked in the flush context and released on dispose. +- Source image texture views are cached per flush context to avoid duplicate uploads. + +## Destination Writeback and Flush Count + +- `FlushCompositions` performs one command-buffer submission (`QueueSubmit`) per scene flush. +- Destination writeback to the render target occurs via destination blit pass(es) before final submit: + - one blit per prepared batch in command order, + - with destination storage preserved across batches. +- For scenes that plan to a single prepared batch (common case), this is one destination blit pass. ## Fallback Behavior -Fallback is batch-scoped, not scene-scoped: +Fallback is scene-scoped: - if target exposes a CPU region: - - run `DefaultDrawingBackend.FlushPreparedBatch(...)` directly + - run `DefaultDrawingBackend.FlushCompositions(...)` directly - if target is native-surface only: - rent CPU staging frame - - run `DefaultDrawingBackend.FlushPreparedBatch(...)` on staging + - run `DefaultDrawingBackend.FlushCompositions(...)` on staging - upload staging pixels back to native target texture ## Shader Source and Null Terminator -All WGSL sources in this backend are stored as UTF-8 compile-time literals with an explicit trailing U+0000. - -Reason: +All static WGSL shader sources are stored as null-terminated UTF-8 bytes (`U+0000` terminator at call site requirement), including: -- native WebGPU module creation consumes WGSL through a null-terminated pointer -- embedding the terminator in the literal avoids runtime append/copy work -- keeping shader bytes as static literal data removes per-call allocations +- coverage pipeline shaders (`PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine`) +- composition shaders (`PreparedComposite`, `CompositeDestinationInit`, `CompositeDestinationBlit`) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs new file mode 100644 index 000000000..ccd2ae774 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -0,0 +1,2084 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal sealed unsafe partial class WebGPUDrawingBackend +{ + private const int TileWidth = 16; + private const int TileHeight = 16; + private const float TileScale = 1F / TileWidth; + private const int LineStrideBytes = 24; + private const int PathStrideBytes = 32; + private const int TileStrideBytes = 8; + private const int SegmentCountStrideBytes = 8; + private const int SegmentStrideBytes = 24; + private const int SegmentAllocWorkgroupSize = 256; + + private delegate uint BindGroupEntryWriter(Span entries); + + private unsafe delegate void ComputePassDispatch(ComputePassEncoder* pass); + + private bool TryCreateCoverageTextureFromFlattened( + WebGPUFlushContext flushContext, + List definitions, + Configuration configuration, + out TextureView* coverageView, + out CoveragePlacement[] coveragePlacements, + out string? error) + where TPixel : unmanaged, IPixel + { + coverageView = null; + coveragePlacements = Array.Empty(); + error = null; + if (definitions.Count == 0) + { + return true; + } + + CoveragePathBuild[] pathBuilds = new CoveragePathBuild[definitions.Count]; + coveragePlacements = new CoveragePlacement[definitions.Count]; + int totalLineCount = 0; + int totalTileCount = 0; + ulong totalEstimatedSegments = 0; + int atlasWidthInTiles = 0; + int atlasHeightInTiles = 0; + int currentTileY = 0; + uint? fillRuleValue = null; + uint? aliasedValue = null; + try + { + for (int i = 0; i < definitions.Count; i++) + { + CompositionCoverageDefinition definition = definitions[i]; + Rectangle interest = definition.RasterizerOptions.Interest; + if (interest.Width <= 0 || interest.Height <= 0) + { + error = "Invalid coverage bounds."; + return false; + } + + uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; + uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; + if ((fillRuleValue.HasValue && fillRuleValue.Value != fillRule) || + (aliasedValue.HasValue && aliasedValue.Value != isAliased)) + { + error = "Mixed rasterization modes are not supported in one flush coverage pass."; + return false; + } + + fillRuleValue ??= fillRule; + aliasedValue ??= isAliased; + + int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); + int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); + int originTileX = 0; + int originTileY = currentTileY; + int originX = originTileX * TileWidth; + int originY = originTileY * TileHeight; + + if (!TryBuildLineBuffer( + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + configuration.MemoryAllocator, + out IMemoryOwner? lineOwner, + out int lineCount, + out _, + out _, + out _, + out _, + out uint estimatedSegments, + out error)) + { + return false; + } + + pathBuilds[i] = new CoveragePathBuild( + lineOwner, + lineCount, + estimatedSegments, + widthInTiles, + heightInTiles, + originTileX, + originTileY, + originX, + originY, + interest.Width, + interest.Height); + coveragePlacements[i] = new CoveragePlacement(originX, originY, interest.Width, interest.Height); + + totalLineCount = checked(totalLineCount + lineCount); + totalEstimatedSegments += estimatedSegments; + atlasWidthInTiles = Math.Max(atlasWidthInTiles, widthInTiles); + atlasHeightInTiles = Math.Max(atlasHeightInTiles, originTileY + heightInTiles); + currentTileY += heightInTiles; + } + + totalTileCount = checked(atlasWidthInTiles * atlasHeightInTiles); + + int atlasWidth = Math.Max(1, atlasWidthInTiles * TileWidth); + int atlasHeight = Math.Max(1, atlasHeightInTiles * TileHeight); + if (!TryCreateCoverageTexture( + flushContext, + atlasWidth, + atlasHeight, + configuration.MemoryAllocator, + out Texture* coverageTexture, + out coverageView, + out error)) + { + return false; + } + + flushContext.TrackTexture(coverageTexture); + flushContext.TrackTextureView(coverageView); + if (totalLineCount == 0) + { + return true; + } + + int lineBufferBytes = checked(totalLineCount * LineStrideBytes); + using IMemoryOwner lineUploadOwner = configuration.MemoryAllocator.Allocate(lineBufferBytes); + Span lineUpload = lineUploadOwner.Memory.Span[..lineBufferBytes]; + int mergedLineIndex = 0; + for (int pathIndex = 0; pathIndex < pathBuilds.Length; pathIndex++) + { + CoveragePathBuild build = pathBuilds[pathIndex]; + if (build.LineCount == 0 || build.LineOwner is null) + { + continue; + } + + ReadOnlySpan sourceLines = build.LineOwner.Memory.Span[..(build.LineCount * LineStrideBytes)]; + for (int lineIndex = 0; lineIndex < build.LineCount; lineIndex++) + { + int sourceOffset = lineIndex * LineStrideBytes; + float x0 = ReadFloat(sourceLines, sourceOffset + 8) + build.OriginX; + float y0 = ReadFloat(sourceLines, sourceOffset + 12) + build.OriginY; + float x1 = ReadFloat(sourceLines, sourceOffset + 16) + build.OriginX; + float y1 = ReadFloat(sourceLines, sourceOffset + 20) + build.OriginY; + WriteLine(lineUpload, mergedLineIndex, (uint)pathIndex, x0, y0, x1, y1); + mergedLineIndex++; + } + } + + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-lines", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)lineBufferBytes, + out WgpuBuffer* lineBuffer, + out error)) + { + return false; + } + + fixed (byte* linePtr = lineUpload) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + lineBuffer, + 0, + linePtr, + (nuint)lineBufferBytes); + } + + int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); + using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); + Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; + pathUpload.Clear(); + int tileBase = 0; + for (int i = 0; i < pathBuilds.Length; i++) + { + CoveragePathBuild build = pathBuilds[i]; + WritePath( + pathUpload.Slice(i * PathStrideBytes, PathStrideBytes), + (uint)build.OriginTileX, + (uint)build.OriginTileY, + (uint)(build.OriginTileX + atlasWidthInTiles), + (uint)(build.OriginTileY + build.HeightInTiles), + (uint)tileBase); + tileBase = checked(tileBase + (atlasWidthInTiles * build.HeightInTiles)); + } + + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-paths", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)pathBufferBytes, + out WgpuBuffer* pathBuffer, + out error)) + { + return false; + } + + fixed (byte* pathPtr = pathUpload) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + pathBuffer, + 0, + pathPtr, + (nuint)pathBufferBytes); + } + + int tileBufferBytes = checked(totalTileCount * TileStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-tiles", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileBufferBytes, + out WgpuBuffer* tileBuffer, + out error)) + { + return false; + } + + using (IMemoryOwner tileZeroOwner = configuration.MemoryAllocator.Allocate(tileBufferBytes)) + { + Span tileZero = tileZeroOwner.Memory.Span[..tileBufferBytes]; + tileZero.Clear(); + fixed (byte* tilePtr = tileZero) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileBuffer, + 0, + tilePtr, + (nuint)tileBufferBytes); + } + } + + int tileCountsBytes = checked(totalTileCount * sizeof(uint)); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-tile-counts", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileCountsBytes, + out WgpuBuffer* tileCountsBuffer, + out error)) + { + return false; + } + + using (IMemoryOwner tileCountsZeroOwner = configuration.MemoryAllocator.Allocate(tileCountsBytes)) + { + Span tileCountsZero = tileCountsZeroOwner.Memory.Span[..tileCountsBytes]; + tileCountsZero.Clear(); + fixed (byte* tileCountsPtr = tileCountsZero) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileCountsBuffer, + 0, + tileCountsPtr, + (nuint)tileCountsBytes); + } + } + + if (totalEstimatedSegments > int.MaxValue) + { + error = "Coverage segment estimate overflow."; + return false; + } + + uint segCountsCapacity = totalEstimatedSegments == 0 ? 1u : checked((uint)totalEstimatedSegments); + uint segmentsCapacity = segCountsCapacity; + int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segment-counts", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)segCountsBytes, + out WgpuBuffer* segCountsBuffer, + out error)) + { + return false; + } + + using (IMemoryOwner segCountsZeroOwner = configuration.MemoryAllocator.Allocate(segCountsBytes)) + { + Span segCountsZero = segCountsZeroOwner.Memory.Span[..segCountsBytes]; + segCountsZero.Clear(); + fixed (byte* segCountsPtr = segCountsZero) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + segCountsBuffer, + 0, + segCountsPtr, + (nuint)segCountsBytes); + } + } + + int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segments", + BufferUsage.Storage, + (nuint)segmentsBytes, + out WgpuBuffer* segmentsBuffer, + out error)) + { + return false; + } + + RasterConfig config = new() + { + WidthInTiles = (uint)atlasWidthInTiles, + HeightInTiles = (uint)atlasHeightInTiles, + TargetWidth = (uint)atlasWidth, + TargetHeight = (uint)atlasHeight, + BaseColor = 0, + NDrawObj = 0, + NPath = (uint)pathBuilds.Length, + NClip = 0, + BinDataStart = 0, + PathtagBase = 0, + PathdataBase = 0, + DrawtagBase = 0, + DrawdataBase = 0, + TransformBase = 0, + StyleBase = 0, + LinesSize = (uint)totalLineCount, + BinningSize = (uint)pathBuilds.Length, + TilesSize = (uint)totalTileCount, + SegCountsSize = segCountsCapacity, + SegmentsSize = segmentsCapacity, + BlendSize = 1, + PtclSize = 1 + }; + + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-raster-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* configBuffer, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, (nuint)Unsafe.SizeOf()); + + BumpAllocatorsData bumpData = new() + { + Failed = 0, + Binning = 0, + Ptcl = 0, + Tile = 0, + SegCounts = 0, + Segments = 0, + Blend = 0, + Lines = (uint)totalLineCount + }; + + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-bump", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* bumpBuffer, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, bumpBuffer, 0, &bumpData, (nuint)Unsafe.SizeOf()); + + IndirectCountData indirectData = default; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-indirect", + BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* indirectBuffer, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, indirectBuffer, 0, &indirectData, (nuint)Unsafe.SizeOf()); + + SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)totalTileCount }; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segment-alloc", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* segmentAllocBuffer, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, segmentAllocBuffer, 0, &segmentAllocConfig, (nuint)Unsafe.SizeOf()); + + CoverageConfig coverageConfig = new() + { + TargetWidth = (uint)atlasWidth, + TargetHeight = (uint)atlasHeight, + TileOriginX = 0, + TileOriginY = 0, + TileWidthInTiles = (uint)atlasWidthInTiles, + TileHeightInTiles = (uint)atlasHeightInTiles, + FillRule = fillRuleValue.GetValueOrDefault(0), + IsAliased = aliasedValue.GetValueOrDefault(0) + }; + + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-coverage-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* coverageConfigBuffer, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, coverageConfigBuffer, 0, &coverageConfig, (nuint)Unsafe.SizeOf()); + + if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error) || + !this.DispatchPathCount(flushContext, configBuffer, bumpBuffer, lineBuffer, pathBuffer, tileBuffer, segCountsBuffer, indirectBuffer, out error) || + !this.DispatchBackdrop(flushContext, configBuffer, tileBuffer, atlasHeightInTiles, out error) || + !this.DispatchSegmentAlloc(flushContext, bumpBuffer, tileBuffer, tileCountsBuffer, segmentAllocBuffer, totalTileCount, out error) || + !this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error) || + !this.DispatchPathTiling(flushContext, bumpBuffer, segCountsBuffer, lineBuffer, pathBuffer, tileBuffer, segmentsBuffer, indirectBuffer, out error) || + !this.DispatchCoverageFine(flushContext, coverageConfigBuffer, tileBuffer, tileCountsBuffer, segmentsBuffer, coverageView, atlasWidthInTiles, atlasHeightInTiles, out error)) + { + return false; + } + + error = null; + return true; + } + finally + { + for (int i = 0; i < pathBuilds.Length; i++) + { + pathBuilds[i].LineOwner?.Dispose(); + } + } + } + + private bool TryCreateCoverageTextureFromFlattened( + WebGPUFlushContext flushContext, + in CompositionCoverageDefinition definition, + Configuration configuration, + out TextureView* coverageView, + out string? error) + where TPixel : unmanaged, IPixel + { + coverageView = null; + error = null; + + Rectangle interest = definition.RasterizerOptions.Interest; + if (interest.Width <= 0 || interest.Height <= 0) + { + error = "Invalid coverage bounds."; + return false; + } + + IMemoryOwner? lineOwner = null; + try + { + if (!TryBuildLineBuffer( + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + configuration.MemoryAllocator, + out lineOwner, + out int lineCount, + out float minX, + out float minY, + out float maxX, + out float maxY, + out uint estimatedSegments, + out error)) + { + return false; + } + + if (!TryCreateCoverageTexture( + flushContext, + interest.Width, + interest.Height, + configuration.MemoryAllocator, + out Texture* coverageTexture, + out coverageView, + out error)) + { + return false; + } + + flushContext.TrackTexture(coverageTexture); + flushContext.TrackTextureView(coverageView); + + if (lineCount == 0) + { + return true; + } + + int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); + int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); + int tileMinX = 0; + int tileMinY = 0; + int tileMaxX = widthInTiles; + int tileMaxY = heightInTiles; + + int tileWidth = tileMaxX - tileMinX; + int tileHeight = tileMaxY - tileMinY; + if (tileWidth <= 0 || tileHeight <= 0) + { + return true; + } + + int tileCount = checked(tileWidth * tileHeight); + uint segCountsCapacity = estimatedSegments == 0 ? 1u : estimatedSegments; + uint segmentsCapacity = segCountsCapacity; + + int lineBufferBytes = checked(lineCount * LineStrideBytes); + BufferDescriptor lineDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)lineBufferBytes + }; + + WgpuBuffer* lineBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in lineDescriptor); + if (lineBuffer is null) + { + error = "Failed to create line buffer."; + return false; + } + + flushContext.TrackBuffer(lineBuffer); + if (lineOwner is null) + { + error = "Missing line buffer allocation."; + return false; + } + + Span lineBytes = lineOwner.Memory.Span[..lineBufferBytes]; + fixed (byte* linePtr = lineBytes) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + lineBuffer, + 0, + linePtr, + (nuint)lineBufferBytes); + } + + Span pathBytes = stackalloc byte[PathStrideBytes]; + pathBytes.Clear(); + WritePath(pathBytes, (uint)tileMinX, (uint)tileMinY, (uint)tileMaxX, (uint)tileMaxY); + + BufferDescriptor pathDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)PathStrideBytes + }; + + WgpuBuffer* pathBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in pathDescriptor); + if (pathBuffer is null) + { + error = "Failed to create path buffer."; + return false; + } + + flushContext.TrackBuffer(pathBuffer); + fixed (byte* pathPtr = pathBytes) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + pathBuffer, + 0, + pathPtr, + (nuint)PathStrideBytes); + } + + int tileBufferBytes = checked(tileCount * TileStrideBytes); + BufferDescriptor tileDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)tileBufferBytes + }; + + WgpuBuffer* tileBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in tileDescriptor); + if (tileBuffer is null) + { + error = "Failed to create tile buffer."; + return false; + } + + flushContext.TrackBuffer(tileBuffer); + using (IMemoryOwner tileZeroOwner = configuration.MemoryAllocator.Allocate(tileBufferBytes)) + { + Span tileZero = tileZeroOwner.Memory.Span[..tileBufferBytes]; + tileZero.Clear(); + fixed (byte* tilePtr = tileZero) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileBuffer, + 0, + tilePtr, + (nuint)tileBufferBytes); + } + } + + int tileCountsBytes = checked(tileCount * sizeof(uint)); + BufferDescriptor tileCountsDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)tileCountsBytes + }; + + WgpuBuffer* tileCountsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in tileCountsDescriptor); + if (tileCountsBuffer is null) + { + error = "Failed to create tile counts buffer."; + return false; + } + + flushContext.TrackBuffer(tileCountsBuffer); + using (IMemoryOwner tileCountsZeroOwner = configuration.MemoryAllocator.Allocate(tileCountsBytes)) + { + Span tileCountsZero = tileCountsZeroOwner.Memory.Span[..tileCountsBytes]; + tileCountsZero.Clear(); + fixed (byte* tileCountsPtr = tileCountsZero) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileCountsBuffer, + 0, + tileCountsPtr, + (nuint)tileCountsBytes); + } + } + + int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); + BufferDescriptor segCountsDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)segCountsBytes + }; + + WgpuBuffer* segCountsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segCountsDescriptor); + if (segCountsBuffer is null) + { + error = "Failed to create segment counts buffer."; + return false; + } + + flushContext.TrackBuffer(segCountsBuffer); + + int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); + BufferDescriptor segmentsDescriptor = new() + { + Usage = BufferUsage.Storage, + Size = (nuint)segmentsBytes + }; + + WgpuBuffer* segmentsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segmentsDescriptor); + if (segmentsBuffer is null) + { + error = "Failed to create segments buffer."; + return false; + } + + flushContext.TrackBuffer(segmentsBuffer); + + RasterConfig config = new() + { + WidthInTiles = (uint)widthInTiles, + HeightInTiles = (uint)heightInTiles, + TargetWidth = (uint)interest.Width, + TargetHeight = (uint)interest.Height, + BaseColor = 0, + NDrawObj = 0, + NPath = 1, + NClip = 0, + BinDataStart = 0, + PathtagBase = 0, + PathdataBase = 0, + DrawtagBase = 0, + DrawdataBase = 0, + TransformBase = 0, + StyleBase = 0, + LinesSize = (uint)lineCount, + BinningSize = 1, + TilesSize = (uint)tileCount, + SegCountsSize = segCountsCapacity, + SegmentsSize = segmentsCapacity, + BlendSize = 1, + PtclSize = 1 + }; + + BufferDescriptor configDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* configBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in configDescriptor); + if (configBuffer is null) + { + error = "Failed to create config buffer."; + return false; + } + + flushContext.TrackBuffer(configBuffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + configBuffer, + 0, + &config, + (nuint)Unsafe.SizeOf()); + + BumpAllocatorsData bumpData = new() + { + Failed = 0, + Binning = 0, + Ptcl = 0, + Tile = 0, + SegCounts = 0, + Segments = 0, + Blend = 0, + Lines = (uint)lineCount + }; + + BufferDescriptor bumpDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* bumpBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in bumpDescriptor); + if (bumpBuffer is null) + { + error = "Failed to create bump buffer."; + return false; + } + + flushContext.TrackBuffer(bumpBuffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + bumpBuffer, + 0, + &bumpData, + (nuint)Unsafe.SizeOf()); + + IndirectCountData indirectData = default; + BufferDescriptor indirectDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* indirectBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in indirectDescriptor); + if (indirectBuffer is null) + { + error = "Failed to create indirect dispatch buffer."; + return false; + } + + flushContext.TrackBuffer(indirectBuffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + indirectBuffer, + 0, + &indirectData, + (nuint)Unsafe.SizeOf()); + + SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)tileCount }; + BufferDescriptor segmentAllocDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* segmentAllocBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segmentAllocDescriptor); + if (segmentAllocBuffer is null) + { + error = "Failed to create segment allocation buffer."; + return false; + } + + flushContext.TrackBuffer(segmentAllocBuffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + segmentAllocBuffer, + 0, + &segmentAllocConfig, + (nuint)Unsafe.SizeOf()); + + uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; + uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; + CoverageConfig coverageConfig = new() + { + TargetWidth = (uint)interest.Width, + TargetHeight = (uint)interest.Height, + TileOriginX = (uint)tileMinX, + TileOriginY = (uint)tileMinY, + TileWidthInTiles = (uint)tileWidth, + TileHeightInTiles = (uint)tileHeight, + FillRule = fillRule, + IsAliased = isAliased + }; + + BufferDescriptor coverageConfigDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* coverageConfigBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in coverageConfigDescriptor); + if (coverageConfigBuffer is null) + { + error = "Failed to create coverage config buffer."; + return false; + } + + flushContext.TrackBuffer(coverageConfigBuffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + coverageConfigBuffer, + 0, + &coverageConfig, + (nuint)Unsafe.SizeOf()); + + if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error)) + { + return false; + } + + if (!this.DispatchPathCount( + flushContext, + configBuffer, + bumpBuffer, + lineBuffer, + pathBuffer, + tileBuffer, + segCountsBuffer, + indirectBuffer, + out error)) + { + return false; + } + + if (!this.DispatchBackdrop( + flushContext, + configBuffer, + tileBuffer, + heightInTiles, + out error)) + { + return false; + } + + if (!this.DispatchSegmentAlloc( + flushContext, + bumpBuffer, + tileBuffer, + tileCountsBuffer, + segmentAllocBuffer, + tileCount, + out error)) + { + return false; + } + + if (!this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error)) + { + return false; + } + + if (!this.DispatchPathTiling( + flushContext, + bumpBuffer, + segCountsBuffer, + lineBuffer, + pathBuffer, + tileBuffer, + segmentsBuffer, + indirectBuffer, + out error)) + { + return false; + } + + if (!this.DispatchCoverageFine( + flushContext, + coverageConfigBuffer, + tileBuffer, + tileCountsBuffer, + segmentsBuffer, + coverageView, + tileWidth, + tileHeight, + out error)) + { + return false; + } + + error = null; + return true; + } + finally + { + lineOwner?.Dispose(); + } + } + + private static bool TryGetOrCreateCoverageBuffer( + WebGPUFlushContext flushContext, + string bufferKey, + BufferUsage usage, + nuint requiredSize, + out WgpuBuffer* buffer, + out string? error) + => flushContext.DeviceState.TryGetOrCreateSharedBuffer( + bufferKey, + usage, + requiredSize, + out buffer, + out _, + out error); + + private static bool TryBuildLineBuffer( + IPath path, + in Rectangle interest, + RasterizerSamplingOrigin samplingOrigin, + MemoryAllocator allocator, + out IMemoryOwner? lineOwner, + out int lineCount, + out float minX, + out float minY, + out float maxX, + out float maxY, + out uint estimatedSegments, + out string? error) + { + error = null; + lineOwner = null; + lineCount = 0; + estimatedSegments = 0; + minX = float.PositiveInfinity; + minY = float.PositiveInfinity; + maxX = float.NegativeInfinity; + maxY = float.NegativeInfinity; + bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + + List simplePaths = []; + foreach (ISimplePath simplePath in path.Flatten()) + { + simplePaths.Add(simplePath); + } + + for (int i = 0; i < simplePaths.Count; i++) + { + ReadOnlySpan points = simplePaths[i].Points.Span; + if (points.Length < 2) + { + continue; + } + + for (int j = 0; j < points.Length; j++) + { + float x = (points[j].X - interest.X) + samplingOffsetX; + float y = (points[j].Y - interest.Y) + samplingOffsetY; + if (x < minX) + { + minX = x; + } + + if (y < minY) + { + minY = y; + } + + if (x > maxX) + { + maxX = x; + } + + if (y > maxY) + { + maxY = y; + } + } + + int contourSegmentCount = simplePaths[i].IsClosed + ? points.Length + : points.Length - 1; + if (contourSegmentCount <= 0) + { + continue; + } + + lineCount += contourSegmentCount; + } + + if (lineCount == 0) + { + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; + return true; + } + + int lineBufferBytes = checked(lineCount * LineStrideBytes); + lineOwner = allocator.Allocate(lineBufferBytes); + Span lineBytes = lineOwner.Memory.Span[..lineBufferBytes]; + lineBytes.Clear(); + + int lineIndex = 0; + for (int i = 0; i < simplePaths.Count; i++) + { + ReadOnlySpan points = simplePaths[i].Points.Span; + if (points.Length < 2) + { + continue; + } + + bool contourClosed = simplePaths[i].IsClosed; + int segmentCount = contourClosed + ? points.Length + : points.Length - 1; + if (segmentCount <= 0) + { + continue; + } + + for (int j = 0; j < segmentCount; j++) + { + PointF p0 = points[j]; + int nextIndex = j + 1; + if (nextIndex == points.Length) + { + nextIndex = 0; + } + + PointF p1 = points[nextIndex]; + float x0 = (p0.X - interest.X) + samplingOffsetX; + float y0 = (p0.Y - interest.Y) + samplingOffsetY; + float x1 = (p1.X - interest.X) + samplingOffsetX; + float y1 = (p1.Y - interest.Y) + samplingOffsetY; + WriteLine(lineBytes, lineIndex, x0, y0, x1, y1); + estimatedSegments += EstimateSegmentCount(x0, y0, x1, y1); + lineIndex++; + } + } + + return true; + } + + private static void WriteLine(Span destination, int lineIndex, float x0, float y0, float x1, float y1) + => WriteLine(destination, lineIndex, 0u, x0, y0, x1, y1); + + private static void WriteLine(Span destination, int lineIndex, uint pathIndex, float x0, float y0, float x1, float y1) + { + int offset = lineIndex * LineStrideBytes; + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), pathIndex); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset + 4, 4), 0u); + WriteFloat(destination, offset + 8, x0); + WriteFloat(destination, offset + 12, y0); + WriteFloat(destination, offset + 16, x1); + WriteFloat(destination, offset + 20, y1); + } + + private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1) + => WritePath(destination, x0, y0, x1, y1, 0u); + + private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1, uint tiles) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(0, 4), x0); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4, 4), y0); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8, 4), x1); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), y1); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16, 4), tiles); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float ReadFloat(ReadOnlySpan source, int offset) + => BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(source.Slice(offset, 4))); + + private static void WriteFloat(Span destination, int offset, float value) + => BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), (uint)BitConverter.SingleToInt32Bits(value)); + + private static uint EstimateSegmentCount(float x0, float y0, float x1, float y1) + { + float s0x = x0 * TileScale; + float s0y = y0 * TileScale; + float s1x = x1 * TileScale; + float s1y = y1 * TileScale; + uint countX = SpanTiles(s0x, s1x); + uint countY = SpanTiles(s0y, s1y); + if (countX > 0) + { + countX -= 1; + } + + return countX + countY; + } + + private static uint SpanTiles(float a, float b) + { + float max = MathF.Max(a, b); + float min = MathF.Min(a, b); + float span = MathF.Ceiling(max) - MathF.Floor(min); + if (span < 1F) + { + span = 1F; + } + + return (uint)span; + } + + private static int Clamp(int value, int min, int max) + { + if (value < min) + { + return min; + } + + return value > max ? max : value; + } + + private static bool TryCreateCoverageTexture( + WebGPUFlushContext flushContext, + int width, + int height, + MemoryAllocator allocator, + out Texture* coverageTexture, + out TextureView* coverageView, + out string? error) + { + TextureDescriptor descriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = TextureFormat.R32float, + MipLevelCount = 1, + SampleCount = 1 + }; + + coverageTexture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); + if (coverageTexture is null) + { + coverageView = null; + error = "Failed to create coverage texture."; + return false; + } + + TextureViewDescriptor viewDescriptor = new() + { + Format = descriptor.Format, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + coverageView = flushContext.Api.TextureCreateView(coverageTexture, in viewDescriptor); + if (coverageView is null) + { + flushContext.Api.TextureRelease(coverageTexture); + error = "Failed to create coverage texture view."; + return false; + } + + int rowBytes = checked(width * sizeof(float)); + int byteCount = checked(rowBytes * height); + using (IMemoryOwner zeroOwner = allocator.Allocate(byteCount)) + { + Span zeroData = zeroOwner.Memory.Span[..byteCount]; + zeroData.Clear(); + ImageCopyTexture destination = new() + { + Texture = coverageTexture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + Extent3D writeSize = new((uint)width, (uint)height, 1); + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)rowBytes, + RowsPerImage = (uint)height + }; + + fixed (byte* zeroPtr = zeroData) + { + flushContext.Api.QueueWriteTexture( + flushContext.Queue, + in destination, + zeroPtr, + (nuint)byteCount, + in layout, + in writeSize); + } + } + + error = null; + return true; + } + + private bool DispatchPathCountSetup( + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* indirectBuffer, + out string? error) + => this.DispatchComputePass( + flushContext, + "path-count-setup", + PathCountSetupComputeShader.Code, + TryCreatePathCountSetupBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = indirectBuffer, Offset = 0, Size = nuint.MaxValue }; + return 2; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), + out error); + + private bool DispatchPathCount( + WebGPUFlushContext flushContext, + WgpuBuffer* configBuffer, + WgpuBuffer* bumpBuffer, + WgpuBuffer* lineBuffer, + WgpuBuffer* pathBuffer, + WgpuBuffer* tileBuffer, + WgpuBuffer* segCountsBuffer, + WgpuBuffer* indirectBuffer, + out string? error) + => this.DispatchComputePass( + flushContext, + "path-count", + PathCountComputeShader.Code, + TryCreatePathCountBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = configBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = lineBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = pathBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[5] = new BindGroupEntry { Binding = 5, Buffer = segCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + return 6; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, (nuint)0), + out error); + + private bool DispatchSegmentAlloc( + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* tileBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* segmentAllocBuffer, + int tileCount, + out string? error) + => this.DispatchComputePass( + flushContext, + "segment-alloc", + SegmentAllocComputeShader.Code, + TryCreateSegmentAllocBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = segmentAllocBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 4; + }, + (pass) => + { + uint dispatchX = DivideRoundUp(tileCount, SegmentAllocWorkgroupSize); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, dispatchX, 1, 1); + }, + out error); + + private bool DispatchBackdrop( + WebGPUFlushContext flushContext, + WgpuBuffer* configBuffer, + WgpuBuffer* tileBuffer, + int heightInTiles, + out string? error) + => this.DispatchComputePass( + flushContext, + "backdrop", + BackdropComputeShader.Code, + TryCreateBackdropBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = configBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; + return 2; + }, + (pass) => + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)heightInTiles, 1, 1); + }, + out error); + + private bool DispatchPathTilingSetup( + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* indirectBuffer, + out string? error) + => this.DispatchComputePass( + flushContext, + "path-tiling-setup", + PathTilingSetupComputeShader.Code, + TryCreatePathTilingSetupBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = indirectBuffer, Offset = 0, Size = nuint.MaxValue }; + return 2; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), + out error); + + private bool DispatchPathTiling( + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* segCountsBuffer, + WgpuBuffer* lineBuffer, + WgpuBuffer* pathBuffer, + WgpuBuffer* tileBuffer, + WgpuBuffer* segmentsBuffer, + WgpuBuffer* indirectBuffer, + out string? error) + => this.DispatchComputePass( + flushContext, + "path-tiling", + PathTilingComputeShader.Code, + TryCreatePathTilingBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = segCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = lineBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = pathBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[5] = new BindGroupEntry { Binding = 5, Buffer = segmentsBuffer, Offset = 0, Size = nuint.MaxValue }; + return 6; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, (nuint)0), + out error); + + private bool DispatchCoverageFine( + WebGPUFlushContext flushContext, + WgpuBuffer* coverageConfigBuffer, + WgpuBuffer* tileBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* segmentsBuffer, + TextureView* coverageView, + int tileWidth, + int tileHeight, + out string? error) + => this.DispatchComputePass( + flushContext, + "coverage-fine", + CoverageFineComputeShader.Code, + TryCreateCoverageFineBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = coverageConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = segmentsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, TextureView = coverageView }; + return 5; + }, + (pass) => + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)tileWidth, (uint)tileHeight, 1); + }, + out error); + + private bool DispatchComputePass( + WebGPUFlushContext flushContext, + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + BindGroupEntryWriter entryWriter, + ComputePassDispatch dispatch, + out string? error) + { + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + pipelineKey, + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + BindGroupEntry* entries = stackalloc BindGroupEntry[8]; + uint entryCount = entryWriter(new Span(entries, 8)); + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = entryCount, + Entries = entries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = $"Failed to create bind group for pipeline '{pipelineKey}'."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = $"Failed to begin compute pass for pipeline '{pipelineKey}'."; + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + dispatch(passEncoder); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + error = null; + return true; + } + + private static bool TryCreatePathCountSetupBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create path count setup bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePathCountBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)LineStrideBytes + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)PathStrideBytes + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)TileStrideBytes + } + }; + entries[5] = new BindGroupLayoutEntry + { + Binding = 5, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)SegmentCountStrideBytes + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create path count bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreateSegmentAllocBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)TileStrideBytes + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = sizeof(uint) + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create segment allocation bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreateBackdropBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create backdrop bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePathTilingSetupBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create path tiling setup bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePathTilingBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)SegmentCountStrideBytes + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)LineStrideBytes + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)PathStrideBytes + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)TileStrideBytes + } + }; + entries[5] = new BindGroupLayoutEntry + { + Binding = 5, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = (nuint)SegmentStrideBytes + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create path tiling bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreateCoverageFineBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)TileStrideBytes + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = sizeof(uint) + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = (nuint)SegmentStrideBytes + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + StorageTexture = new StorageTextureBindingLayout + { + Access = StorageTextureAccess.WriteOnly, + Format = TextureFormat.R32float, + ViewDimension = TextureViewDimension.Dimension2D + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 5, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create coverage fine bind group layout."; + return false; + } + + error = null; + return true; + } + + private readonly struct CoveragePathBuild + { + public CoveragePathBuild( + IMemoryOwner? lineOwner, + int lineCount, + uint estimatedSegments, + int widthInTiles, + int heightInTiles, + int originTileX, + int originTileY, + int originX, + int originY, + int coverageWidth, + int coverageHeight) + { + this.LineOwner = lineOwner; + this.LineCount = lineCount; + this.EstimatedSegments = estimatedSegments; + this.WidthInTiles = widthInTiles; + this.HeightInTiles = heightInTiles; + this.OriginTileX = originTileX; + this.OriginTileY = originTileY; + this.OriginX = originX; + this.OriginY = originY; + this.CoverageWidth = coverageWidth; + this.CoverageHeight = coverageHeight; + } + + public IMemoryOwner? LineOwner { get; } + + public int LineCount { get; } + + public uint EstimatedSegments { get; } + + public int WidthInTiles { get; } + + public int HeightInTiles { get; } + + public int OriginTileX { get; } + + public int OriginTileY { get; } + + public int OriginX { get; } + + public int OriginY { get; } + + public int CoverageWidth { get; } + + public int CoverageHeight { get; } + } + + [StructLayout(LayoutKind.Sequential)] + private struct RasterConfig + { + public uint WidthInTiles; + public uint HeightInTiles; + public uint TargetWidth; + public uint TargetHeight; + public uint BaseColor; + public uint NDrawObj; + public uint NPath; + public uint NClip; + public uint BinDataStart; + public uint PathtagBase; + public uint PathdataBase; + public uint DrawtagBase; + public uint DrawdataBase; + public uint TransformBase; + public uint StyleBase; + public uint LinesSize; + public uint BinningSize; + public uint TilesSize; + public uint SegCountsSize; + public uint SegmentsSize; + public uint BlendSize; + public uint PtclSize; + public uint Pad0; + public uint Pad1; + } + + [StructLayout(LayoutKind.Sequential)] + private struct BumpAllocatorsData + { + public uint Failed; + public uint Binning; + public uint Ptcl; + public uint Tile; + public uint SegCounts; + public uint Segments; + public uint Blend; + public uint Lines; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IndirectCountData + { + public uint CountX; + public uint CountY; + public uint CountZ; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SegmentAllocConfig + { + public uint TileCount; + public uint Pad0; + public uint Pad1; + public uint Pad2; + } + + [StructLayout(LayoutKind.Sequential)] + private struct CoverageConfig + { + public uint TargetWidth; + public uint TargetHeight; + public uint TileOriginX; + public uint TileOriginY; + public uint TileWidthInTiles; + public uint TileHeightInTiles; + public uint FillRule; + public uint IsAliased; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs deleted file mode 100644 index fdadefbf2..000000000 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.TiledComposite.cs +++ /dev/null @@ -1,1076 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Silk.NET.WebGPU; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal sealed unsafe partial class WebGPUDrawingBackend -{ - private const int TiledCompositeTileSize = CompositeComputeWorkgroupSize; - private const string TiledCompositePipelineKey = "tiled-composite"; - - /// - /// Composes one brush command into GPU brush/source-layer data consumed by the tiled compute shader. - /// - /// The destination/source pixel format. - private interface ITiledCompositeBrushComposer - where TPixel : unmanaged, IPixel - { - /// - /// Converts one prepared command into brush data and source-layer bindings. - /// - /// The prepared command being encoded. - /// The active flush context for target-space mapping. - /// Shared build context accumulating source layers and brush data. - /// - /// The destination-local composition bounds used to map brush-space origins into destination-buffer space. - /// - /// The encoded brush-data index for the command. - /// Failure reason when conversion cannot complete. - /// when conversion succeeds; otherwise . - public bool TryCompose( - PreparedCompositionCommand command, - WebGPUFlushContext flushContext, - TiledCompositeBuildContext buildContext, - in Rectangle compositionBounds, - out int brushDataIndex, - out string? error); - } - - /// - /// Composites one prepared batch using the tiled compute path. - /// - /// The destination pixel format. - /// The active flush context for the current frame target. - /// The prepared GPU coverage texture view. - /// The prepared composition commands to apply in order. - /// The destination-local bounds to initialize/compose/read back for this batch. - /// - /// Indicates whether destination storage should be blitted back to the target texture after this batch. - /// - /// Receives an error message when composition fails. - /// when composition succeeds; otherwise . - private bool TryCompositeBatchTiled( - WebGPUFlushContext flushContext, - TextureView* coverageView, - IReadOnlyList commands, - Rectangle? compositionBounds, - bool blitToTarget, - out string? error) - where TPixel : unmanaged, IPixel - { - error = null; - if (commands.Count == 0) - { - return true; - } - - Rectangle frameLocalBounds = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); - Rectangle targetLocalBounds = compositionBounds is Rectangle requestedBounds - ? Rectangle.Intersect(frameLocalBounds, requestedBounds) - : frameLocalBounds; - if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) - { - return true; - } - - if (!flushContext.EnsureCommandEncoder()) - { - error = "Failed to create WebGPU command encoder."; - return false; - } - - if (flushContext.TargetTexture is null || flushContext.TargetView is null || coverageView is null) - { - error = "WebGPU flush context does not expose required target/coverage resources."; - return false; - } - - // Reuse destination storage across batches in the same flush session when available. - WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; - nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; - if (destinationPixelsBuffer is not null && - (flushContext.CompositeDestinationWidth != targetLocalBounds.Width || - flushContext.CompositeDestinationHeight != targetLocalBounds.Height)) - { - error = "Mismatched composition bounds detected for a reused destination pixel buffer."; - return false; - } - - if (destinationPixelsBuffer is null) - { - TextureView* sourceTextureView = flushContext.TargetView; - int sourceOriginX = targetLocalBounds.X; - int sourceOriginY = targetLocalBounds.Y; - if (!flushContext.CanSampleTargetTexture) - { - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out Texture* sourceTexture, - out sourceTextureView, - out error)) - { - return false; - } - - // When the target cannot be sampled directly, copy into a transient sampling texture first. - CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); - sourceOriginX = 0; - sourceOriginY = 0; - } - - if (!TryCreateDestinationPixelsBuffer( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out destinationPixelsBuffer, - out destinationPixelsByteSize, - out error) || - !TryInitializeDestinationPixels( - flushContext, - sourceTextureView, - destinationPixelsBuffer, - targetLocalBounds, - sourceOriginX, - sourceOriginY, - destinationPixelsByteSize, - out error)) - { - return false; - } - - flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; - flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; - flushContext.CompositeDestinationWidth = targetLocalBounds.Width; - flushContext.CompositeDestinationHeight = targetLocalBounds.Height; - } - - if (!this.TryRunTiledCompositeComputePass( - flushContext, - coverageView, - destinationPixelsBuffer, - destinationPixelsByteSize, - commands, - targetLocalBounds, - out error)) - { - return false; - } - - this.TestingComputePathBatchCount++; - - if (blitToTarget && - !TryBlitDestinationPixelsToTarget( - flushContext, - destinationPixelsBuffer, - destinationPixelsByteSize, - targetLocalBounds, - out error)) - { - return false; - } - - return true; - } - - /// - /// Builds tiled command indirection buffers and dispatches the tiled composite compute shader. - /// - /// The destination pixel format. - /// The active flush context. - /// The prepared GPU coverage texture view. - /// The destination storage buffer. - /// The destination storage size in bytes. - /// The prepared composition commands. - /// The destination-local bounds covered by this composition pass. - /// Receives an error message when dispatch fails. - /// on success; otherwise . - private bool TryRunTiledCompositeComputePass( - WebGPUFlushContext flushContext, - TextureView* coverageView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - IReadOnlyList commands, - in Rectangle destinationBounds, - out string? error) - where TPixel : unmanaged, IPixel - { - error = null; - int commandCount = commands.Count; - if (commandCount == 0) - { - return true; - } - - int destinationWidth = destinationBounds.Width; - int destinationHeight = destinationBounds.Height; - int tilesX = (destinationWidth + TiledCompositeTileSize - 1) / TiledCompositeTileSize; - int tilesY = (destinationHeight + TiledCompositeTileSize - 1) / TiledCompositeTileSize; - if (tilesX <= 0 || tilesY <= 0) - { - return true; - } - - int tileCount = checked(tilesX * tilesY); - using IMemoryOwner tileCommandCountsOwner = flushContext.MemoryAllocator.Allocate(tileCount, AllocationOptions.Clean); - Span tileCommandCounts = tileCommandCountsOwner.Memory.Span[..tileCount]; - - TiledCompositeCommandData[] commandData = new TiledCompositeCommandData[commandCount]; - TiledCompositeBuildContext buildContext = new(); - - for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) - { - PreparedCompositionCommand command = commands[commandIndex]; - Rectangle destinationRegion = command.DestinationRegion; - Rectangle localDestinationRegion = new( - destinationRegion.X - destinationBounds.X, - destinationRegion.Y - destinationBounds.Y, - destinationRegion.Width, - destinationRegion.Height); - - if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) - { - continue; - } - - // First pass: count how many commands overlap each tile. - int minTileX = Math.Clamp(localDestinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); - int minTileY = Math.Clamp(localDestinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); - int maxTileX = Math.Clamp((localDestinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); - int maxTileY = Math.Clamp((localDestinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); - for (int tileY = minTileY; tileY <= maxTileY; tileY++) - { - int rowStart = checked(tileY * tilesX); - for (int tileX = minTileX; tileX <= maxTileX; tileX++) - { - tileCommandCounts[rowStart + tileX]++; - } - } - - if (!TryGetBrushComposer(command.Brush, out ITiledCompositeBrushComposer composer)) - { - error = $"Unsupported brush type for tiled composition: '{command.Brush.GetType().FullName}'."; - return false; - } - - if (!composer.TryCompose(command, flushContext, buildContext, destinationBounds, out int brushDataIndex, out error)) - { - return false; - } - - GraphicsOptions options = command.GraphicsOptions; - commandData[commandIndex] = new TiledCompositeCommandData( - command.SourceOffset.X, - command.SourceOffset.Y, - localDestinationRegion.X, - localDestinationRegion.Y, - localDestinationRegion.Width, - localDestinationRegion.Height, - options.BlendPercentage, - (int)options.ColorBlendingMode, - (int)options.AlphaCompositionMode, - brushDataIndex); - } - - // Convert command counts into prefix ranges for compact tile command lists. - TiledCompositeTileRange[] tileRanges = new TiledCompositeTileRange[tileCount]; - int totalTileCommandRefs = 0; - for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) - { - int count = tileCommandCounts[tileIndex]; - tileRanges[tileIndex] = new TiledCompositeTileRange((uint)totalTileCommandRefs, (uint)count); - tileCommandCounts[tileIndex] = totalTileCommandRefs; - totalTileCommandRefs = checked(totalTileCommandRefs + count); - } - - // Second pass: write per-tile command index lists. - uint[] tileCommandIndices = new uint[Math.Max(totalTileCommandRefs, 1)]; - for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) - { - TiledCompositeCommandData command = commandData[commandIndex]; - Rectangle destinationRegion = new( - command.DestinationX, - command.DestinationY, - command.DestinationWidth, - command.DestinationHeight); - if (destinationRegion.Width <= 0 || destinationRegion.Height <= 0) - { - continue; - } - - int minTileX = Math.Clamp(destinationRegion.X / TiledCompositeTileSize, 0, tilesX - 1); - int minTileY = Math.Clamp(destinationRegion.Y / TiledCompositeTileSize, 0, tilesY - 1); - int maxTileX = Math.Clamp((destinationRegion.Right - 1) / TiledCompositeTileSize, 0, tilesX - 1); - int maxTileY = Math.Clamp((destinationRegion.Bottom - 1) / TiledCompositeTileSize, 0, tilesY - 1); - for (int tileY = minTileY; tileY <= maxTileY; tileY++) - { - int rowStart = checked(tileY * tilesX); - for (int tileX = minTileX; tileX <= maxTileX; tileX++) - { - int tileIndex = rowStart + tileX; - int writeIndex = tileCommandCounts[tileIndex]++; - tileCommandIndices[writeIndex] = (uint)commandIndex; - } - } - } - - if (!TryCreateSourceLayerTextureArray(flushContext, buildContext.SourceLayers, out TextureView* sourceLayerView, out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - commandData.AsSpan(), - out WgpuBuffer* commandBuffer, - out nuint commandBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - tileRanges.AsSpan(), - out WgpuBuffer* tileRangeBuffer, - out nuint tileRangeBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - tileCommandIndices.AsSpan(), - out WgpuBuffer* tileCommandIndexBuffer, - out nuint tileCommandIndexBufferBytes, - out error) || - !TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Storage, - CollectionsMarshal.AsSpan(buildContext.BrushData), - out WgpuBuffer* brushDataBuffer, - out nuint brushDataBufferBytes, - out error)) - { - return false; - } - - TiledCompositeParameters parameters = new(destinationWidth, destinationHeight, tilesX, TiledCompositeTileSize); - if (!TryCreateAndUploadBuffer( - flushContext, - BufferUsage.Uniform, - MemoryMarshal.CreateReadOnlySpan(ref parameters, 1), - out WgpuBuffer* parameterBuffer, - out nuint parameterBufferBytes, - out error)) - { - return false; - } - - if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - TiledCompositePipelineKey, - TiledCompositeComputeShader.Code, - TryCreateTiledCompositeBindGroupLayout, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, - out error)) - { - return false; - } - - // Bind all shader inputs in one bind group so each tile dispatch has fixed resource layout. - BindGroupEntry* entries = stackalloc BindGroupEntry[8]; - entries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = coverageView - }; - entries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = commandBuffer, - Offset = 0, - Size = commandBufferBytes - }; - entries[2] = new BindGroupEntry - { - Binding = 2, - Buffer = tileRangeBuffer, - Offset = 0, - Size = tileRangeBufferBytes - }; - entries[3] = new BindGroupEntry - { - Binding = 3, - Buffer = tileCommandIndexBuffer, - Offset = 0, - Size = tileCommandIndexBufferBytes - }; - entries[4] = new BindGroupEntry - { - Binding = 4, - Buffer = brushDataBuffer, - Offset = 0, - Size = brushDataBufferBytes - }; - entries[5] = new BindGroupEntry - { - Binding = 5, - TextureView = sourceLayerView - }; - entries[6] = new BindGroupEntry - { - Binding = 6, - Buffer = destinationPixelsBuffer, - Offset = 0, - Size = destinationPixelsByteSize - }; - entries[7] = new BindGroupEntry - { - Binding = 7, - Buffer = parameterBuffer, - Offset = 0, - Size = parameterBufferBytes - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = bindGroupLayout, - EntryCount = 8, - Entries = entries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - error = "Failed to create tiled composite bind group."; - return false; - } - - flushContext.TrackBindGroup(bindGroup); - - ComputePassDescriptor passDescriptor = default; - ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); - if (passEncoder is null) - { - error = "Failed to begin tiled composite compute pass."; - return false; - } - - try - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, (uint)tilesX, (uint)tilesY, 1); - } - finally - { - flushContext.Api.ComputePassEncoderEnd(passEncoder); - flushContext.Api.ComputePassEncoderRelease(passEncoder); - } - - return true; - } - - /// - /// Builds and uploads the source layer texture array referenced by brush data. - /// - /// The source pixel format. - /// The active flush context. - /// The source layers to upload. - /// Receives the created texture array view. - /// Receives an error message when creation or upload fails. - /// on success; otherwise . - private static bool TryCreateSourceLayerTextureArray( - WebGPUFlushContext flushContext, - List> sourceLayers, - out TextureView* sourceLayerView, - out string? error) - where TPixel : unmanaged, IPixel - { - int layerCount = Math.Max(1, sourceLayers.Count); - int maxWidth = 1; - int maxHeight = 1; - for (int i = 0; i < sourceLayers.Count; i++) - { - TiledSourceLayer layer = sourceLayers[i]; - if (layer.Image is null) - { - continue; - } - - if (layer.Image.Width > maxWidth) - { - maxWidth = layer.Image.Width; - } - - if (layer.Image.Height > maxHeight) - { - maxHeight = layer.Image.Height; - } - } - - TextureDescriptor descriptor = new() - { - Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)maxWidth, (uint)maxHeight, (uint)layerCount), - Format = flushContext.TextureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); - if (texture is null) - { - sourceLayerView = null; - error = "Failed to create source-layer texture array."; - return false; - } - - TextureViewDescriptor viewDescriptor = new() - { - Format = flushContext.TextureFormat, - Dimension = TextureViewDimension.Dimension2DArray, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = (uint)layerCount, - Aspect = TextureAspect.All - }; - - sourceLayerView = flushContext.Api.TextureCreateView(texture, in viewDescriptor); - if (sourceLayerView is null) - { - flushContext.Api.TextureRelease(texture); - error = "Failed to create source-layer texture array view."; - return false; - } - - try - { - if (sourceLayers.Count == 0) - { - // Keep resource bindings valid even when no command produced a source layer. - UploadSolidSourceLayer(flushContext, texture, default(TPixel), 0); - } - else - { - for (int i = 0; i < sourceLayers.Count; i++) - { - TiledSourceLayer layer = sourceLayers[i]; - if (layer.Image is not null) - { - Buffer2DRegion sourceRegion = new(layer.Image.Frames.RootFrame.PixelBuffer, layer.Image.Bounds); - WebGPUFlushContext.UploadTextureFromRegion( - flushContext.Api, - flushContext.Queue, - texture, - sourceRegion, - flushContext.MemoryAllocator, - 0, - 0, - (uint)i); - } - else - { - UploadSolidSourceLayer(flushContext, texture, layer.SolidPixel, (uint)i); - } - } - } - } - catch (Exception ex) - { - flushContext.Api.TextureViewRelease(sourceLayerView); - flushContext.Api.TextureRelease(texture); - sourceLayerView = null; - error = $"Failed to upload source layers for tiled composition. {ex.Message}"; - return false; - } - - flushContext.TrackTexture(texture); - flushContext.TrackTextureView(sourceLayerView); - error = null; - return true; - } - - /// - /// Resolves the brush composer that maps a command brush to tiled shader data. - /// - /// The destination/source pixel format. - /// The command brush. - /// Receives the matching composer when found. - /// when a composer exists; otherwise . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetBrushComposer( - Brush brush, - out ITiledCompositeBrushComposer composer) - where TPixel : unmanaged, IPixel - { - if (brush is ImageBrush) - { - composer = ImageBrushTiledCompositeComposer.Instance; - return true; - } - - if (brush is SolidBrush) - { - composer = SolidBrushTiledCompositeComposer.Instance; - return true; - } - - composer = default!; - return false; - } - - /// - /// Uploads one 1x1 solid-color source layer into the texture array. - /// - private static void UploadSolidSourceLayer( - WebGPUFlushContext flushContext, - Texture* texture, - TPixel pixel, - uint layer) - where TPixel : unmanaged - { - ImageCopyTexture destination = new() - { - Texture = texture, - MipLevel = 0, - Origin = new Origin3D(0, 0, layer), - Aspect = TextureAspect.All - }; - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)Unsafe.SizeOf(), - RowsPerImage = 1 - }; - - Extent3D size = new(1, 1, 1); - TPixel copy = pixel; - flushContext.Api.QueueWriteTexture( - flushContext.Queue, - in destination, - ©, - (nuint)Unsafe.SizeOf(), - in layout, - in size); - } - - /// - /// Allocates one GPU buffer and uploads source data into it. - /// - /// The unmanaged element type. - /// The active flush context. - /// The target buffer usage flags. - /// The data to upload. - /// Receives the created buffer. - /// Receives the allocated byte size. - /// Receives an error message when creation fails. - /// on success; otherwise . - private static bool TryCreateAndUploadBuffer( - WebGPUFlushContext flushContext, - BufferUsage usage, - ReadOnlySpan sourceData, - out WgpuBuffer* buffer, - out nuint bufferSize, - out string? error) - where T : unmanaged - { - nuint elementSize = (nuint)Unsafe.SizeOf(); - nuint writeSize = checked((nuint)sourceData.Length * elementSize); - bufferSize = Math.Max(writeSize, Math.Max(elementSize, 16)); - - BufferDescriptor descriptor = new() - { - Usage = usage | BufferUsage.CopyDst, - Size = bufferSize - }; - - buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); - if (buffer is null) - { - error = "Failed to create tiled composite buffer."; - return false; - } - - flushContext.TrackBuffer(buffer); - if (!sourceData.IsEmpty) - { - fixed (T* sourcePtr = sourceData) - { - flushContext.Api.QueueWriteBuffer(flushContext.Queue, buffer, 0, sourcePtr, writeSize); - } - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by . - /// - private static bool TryCreateTiledCompositeBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[5] = new BindGroupLayoutEntry - { - Binding = 5, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2DArray, - Multisampled = false - } - }; - entries[6] = new BindGroupLayoutEntry - { - Binding = 6, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[7] = new BindGroupLayoutEntry - { - Binding = 7, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 8, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create tiled composite bind-group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Per-command payload consumed by . - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct TiledCompositeCommandData( - int sourceOffsetX, - int sourceOffsetY, - int destinationX, - int destinationY, - int destinationWidth, - int destinationHeight, - float blendPercentage, - int colorBlendingMode, - int alphaCompositionMode, - int brushDataIndex) - { - public readonly int SourceOffsetX = sourceOffsetX; - public readonly int SourceOffsetY = sourceOffsetY; - public readonly int DestinationX = destinationX; - public readonly int DestinationY = destinationY; - public readonly int DestinationWidth = destinationWidth; - public readonly int DestinationHeight = destinationHeight; - public readonly float BlendPercentage = blendPercentage; - public readonly int ColorBlendingMode = colorBlendingMode; - public readonly int AlphaCompositionMode = alphaCompositionMode; - public readonly int BrushDataIndex = brushDataIndex; - public readonly int Padding0 = 0; - public readonly int Padding1 = 0; - } - - /// - /// Per-tile range into the compact tile-command index array. - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct TiledCompositeTileRange(uint startIndex, uint count) - { - public readonly uint StartIndex = startIndex; - public readonly uint Count = count; - } - - /// - /// Brush source sampling payload consumed by . - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct TiledCompositeBrushData( - int sourceRegionX, - int sourceRegionY, - int sourceRegionWidth, - int sourceRegionHeight, - int brushOriginX, - int brushOriginY, - int sourceLayer) - { - public readonly int SourceRegionX = sourceRegionX; - public readonly int SourceRegionY = sourceRegionY; - public readonly int SourceRegionWidth = sourceRegionWidth; - public readonly int SourceRegionHeight = sourceRegionHeight; - public readonly int BrushOriginX = brushOriginX; - public readonly int BrushOriginY = brushOriginY; - public readonly int SourceLayer = sourceLayer; - public readonly int Padding0 = 0; - } - - /// - /// Global dispatch parameters consumed by . - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct TiledCompositeParameters( - int destinationWidth, - int destinationHeight, - int tilesX, - int tileSize) - { - public readonly int DestinationWidth = destinationWidth; - public readonly int DestinationHeight = destinationHeight; - public readonly int TilesX = tilesX; - public readonly int TileSize = tileSize; - } - - /// - /// One tiled source layer entry, either sampled from an image or synthesized from a solid pixel. - /// - private readonly struct TiledSourceLayer - where TPixel : unmanaged, IPixel - { - public TiledSourceLayer(Image image) - { - this.Image = image; - this.SolidPixel = default; - } - - public TiledSourceLayer(TPixel solidPixel) - { - this.Image = null; - this.SolidPixel = solidPixel; - } - - public Image? Image { get; } - - public TPixel SolidPixel { get; } - - public static TiledSourceLayer CreateImage(Image image) => new(image); - - public static TiledSourceLayer CreateSolid(TPixel solidPixel) => new(solidPixel); - } - - /// - /// Brush composer for . - /// - private sealed class SolidBrushTiledCompositeComposer : ITiledCompositeBrushComposer - where TPixel : unmanaged, IPixel - { - public static SolidBrushTiledCompositeComposer Instance { get; } = new(); - - /// - public bool TryCompose( - PreparedCompositionCommand command, - WebGPUFlushContext flushContext, - TiledCompositeBuildContext buildContext, - in Rectangle compositionBounds, - out int brushDataIndex, - out string? error) - { - SolidBrush solidBrush = (SolidBrush)command.Brush; - int sourceLayer = buildContext.GetOrAddSolidLayer(solidBrush); - - brushDataIndex = buildContext.AddBrushData( - new TiledCompositeBrushData( - 0, - 0, - 1, - 1, - 0, - 0, - sourceLayer)); - - error = null; - return true; - } - } - - /// - /// Brush composer for . - /// - private sealed class ImageBrushTiledCompositeComposer : ITiledCompositeBrushComposer - where TPixel : unmanaged, IPixel - { - public static ImageBrushTiledCompositeComposer Instance { get; } = new(); - - /// - public bool TryCompose( - PreparedCompositionCommand command, - WebGPUFlushContext flushContext, - TiledCompositeBuildContext buildContext, - in Rectangle compositionBounds, - out int brushDataIndex, - out string? error) - { - ImageBrush imageBrush = (ImageBrush)command.Brush; - Image sourceImage = (Image)imageBrush.SourceImage; - int sourceLayer = buildContext.GetOrAddImageLayer(sourceImage); - Rectangle sourceRegion = Rectangle.Intersect(sourceImage.Bounds, (Rectangle)imageBrush.SourceRegion); - int brushOriginX = checked(command.BrushBounds.Left + imageBrush.Offset.X - flushContext.TargetBounds.X - compositionBounds.X); - int brushOriginY = checked(command.BrushBounds.Top + imageBrush.Offset.Y - flushContext.TargetBounds.Y - compositionBounds.Y); - - brushDataIndex = buildContext.AddBrushData( - new TiledCompositeBrushData( - sourceRegion.X, - sourceRegion.Y, - sourceRegion.Width, - sourceRegion.Height, - brushOriginX, - brushOriginY, - sourceLayer)); - - error = null; - return true; - } - } - - /// - /// Mutable build context that accumulates deduplicated source layers and brush payloads per batch. - /// - private sealed class TiledCompositeBuildContext - where TPixel : unmanaged, IPixel - { - private readonly Dictionary sourceImageLayers = new(ReferenceEqualityComparer.Instance); - private readonly Dictionary solidColorLayers = []; - private SolidBrush? lastSolidBrush; - private int lastSolidLayer; - - public List BrushData { get; } = []; - - public List> SourceLayers { get; } = []; - - /// - /// Adds brush payload data and returns its index. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int AddBrushData(in TiledCompositeBrushData brushData) - { - int index = this.BrushData.Count; - this.BrushData.Add(brushData); - return index; - } - - /// - /// Gets or creates a source layer index for a solid-color brush payload. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOrAddSolidLayer(SolidBrush solidBrush) - { - if (ReferenceEquals(this.lastSolidBrush, solidBrush)) - { - return this.lastSolidLayer; - } - - TPixel solidPixel = solidBrush.Color.ToPixel(); - if (!this.solidColorLayers.TryGetValue(solidPixel, out int sourceLayer)) - { - sourceLayer = this.SourceLayers.Count; - this.solidColorLayers.Add(solidPixel, sourceLayer); - this.SourceLayers.Add(TiledSourceLayer.CreateSolid(solidPixel)); - } - - this.lastSolidBrush = solidBrush; - this.lastSolidLayer = sourceLayer; - return sourceLayer; - } - - /// - /// Gets or creates a source layer index for an image brush payload. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOrAddImageLayer(Image sourceImage) - { - if (!this.sourceImageLayers.TryGetValue(sourceImage, out int sourceLayer)) - { - sourceLayer = this.SourceLayers.Count; - this.sourceImageLayers.Add(sourceImage, sourceLayer); - this.SourceLayers.Add(TiledSourceLayer.CreateImage(sourceImage)); - } - - return sourceLayer; - } - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index e212cca81..2c0e36536 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -19,7 +21,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; ///
/// /// -/// This backend executes coverage generation and composition on WebGPU where possible and falls back to +/// This backend executes scene composition on WebGPU where possible and falls back to /// when GPU execution is unavailable for a specific command set. /// /// @@ -27,20 +29,13 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// CompositionScene -/// -> CompositionScenePlanner (prepared batches) -/// -> For each batch: -/// 1) Resolve pixel-format handler -/// 2) Acquire flush context (shared session when possible) -/// 3) Prepare/reuse GPU coverage for path definition -/// 4) Composite commands via tiled compute shader into destination pixel buffer -/// 5) Blit to target and optionally read back to CPU region -/// 6) On failure: delegate batch to DefaultDrawingBackend +/// -> Encoded scene stream (draw tags + draw-data stream) +/// -> Acquire flush context +/// -> Execute one tiled scene pass (binning -> coarse -> fine) +/// -> Blit once and optionally read back to CPU region +/// -> On failure: delegate scene to DefaultDrawingBackend /// /// -/// Shared flush sessions allow multiple contiguous GPU-compatible batches to reuse destination initialization -/// and transient GPU resources for one scene flush. -/// -/// /// See src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md for a full process walkthrough. /// /// @@ -49,13 +44,14 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private const uint CompositeVertexCount = 6; private const int CompositeComputeWorkgroupSize = 8; private const int CompositeDestinationPixelStride = 16; + private const uint PreparedBrushTypeSolid = 0; + private const uint PreparedBrushTypeImage = 1; private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; private bool isDisposed; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); - private static int nextSceneFlushId; /// /// Initializes a new instance of the class. @@ -188,214 +184,133 @@ public void FlushCompositions( return; } + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) || + !AreAllCompositionBrushesSupported(compositionScene.Commands)) + { + int fallbackCommandCount = compositionScene.Commands.Count; + this.TestingFallbackPrepareCoverageCallCount += fallbackCommandCount; + this.TestingFallbackCompositeCoverageCallCount += fallbackCommandCount; + this.FlushCompositionsFallback( + configuration, + target, + compositionScene, + target.TryGetCpuRegion(out Buffer2DRegion _), + compositionBounds: null); + return; + } + List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( compositionScene.Commands, target.Bounds); - if (preparedBatches.Count == 0) { return; } - // Shared flush sessions are used only when every command brush is directly supported by GPU composition. - bool supportsSharedFlush = true; - for (int i = 0; i < compositionScene.Commands.Count; i++) + int commandCount = 0; + Rectangle? compositionBounds = null; + for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) { - if (!IsSupportedCompositionBrush(compositionScene.Commands[i].Brush)) + CompositionBatch batch = preparedBatches[batchIndex]; + IReadOnlyList commands = batch.Commands; + for (int i = 0; i < commands.Count; i++) { - supportsSharedFlush = false; - break; + Rectangle destination = commands[i].DestinationRegion; + compositionBounds = compositionBounds.HasValue + ? Rectangle.Union(compositionBounds.Value, destination) + : destination; } - } - int flushId = supportsSharedFlush ? Interlocked.Increment(ref nextSceneFlushId) : 0; - Rectangle? sharedCompositionBounds = null; - if (supportsSharedFlush && TryGetCompositionBounds(preparedBatches, out Rectangle sceneBounds)) - { - sharedCompositionBounds = sceneBounds; + commandCount += commands.Count; } - for (int i = 0; i < preparedBatches.Count; i++) + if (commandCount == 0) { - CompositionBatch batch = preparedBatches[i]; - Rectangle? compositionBounds = sharedCompositionBounds; - if (compositionBounds is null && TryGetCompositionBounds(batch.Commands, out Rectangle batchBounds)) - { - compositionBounds = batchBounds; - } - - this.FlushPreparedBatch( - configuration, - target, - new CompositionBatch( - batch.Definition, - batch.Commands, - flushId, - isFinalBatchInFlush: i == preparedBatches.Count - 1, - compositionBounds)); + return; } - } - /// - /// Executes one prepared composition batch, preferring GPU execution and falling back to CPU when required. - /// - /// The destination pixel format. - /// The active processing configuration. - /// The destination frame. - /// The prepared batch to execute. - private void FlushPreparedBatch( - Configuration configuration, - ICanvasFrame target, - CompositionBatch compositionBatch) - where TPixel : unmanaged, IPixel - { - this.ThrowIfDisposed(); - if (compositionBatch.Commands.Count == 0) + if (compositionBounds is null) { return; } - int commandCount = compositionBatch.Commands.Count; - this.TestingPrepareCoverageCallCount++; - this.TestingReleaseCoverageCallCount++; this.TestingCompositeCoverageCallCount += commandCount; bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); - if (compositionBatch.FlushId == 0 && !AreAllCompositionBrushesSupported(compositionBatch.Commands)) + compositionBounds = Rectangle.Intersect( + compositionBounds.Value, + new Rectangle(0, 0, target.Bounds.Width, target.Bounds.Height)); + if (compositionBounds.Value.Width <= 0 || compositionBounds.Value.Height <= 0) { - this.TestingFallbackPrepareCoverageCallCount++; - this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); return; } - if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration pixelHandler)) - { - this.TestingFallbackPrepareCoverageCallCount++; - this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); - return; - } - - // Flush sessions keep destination state alive across batch boundaries for one scene flush. - bool useFlushSession = compositionBatch.FlushId != 0; bool gpuSuccess = false; bool gpuReady = false; string? failure = null; - bool hadExistingSession = false; - WebGPUFlushContext? flushContext = null; + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + int pixelSizeInBytes = Unsafe.SizeOf(); + using WebGPUFlushContext flushContext = WebGPUFlushContext.Create( + target, + textureFormat, + pixelSizeInBytes, + configuration.MemoryAllocator, + compositionBounds); try { - flushContext = useFlushSession - ? WebGPUFlushContext.GetOrCreateFlushSessionContext( - compositionBatch.FlushId, - target, - pixelHandler.TextureFormat, - pixelHandler.PixelSizeInBytes, - configuration.MemoryAllocator, - compositionBatch.CompositionBounds, - out hadExistingSession) - : WebGPUFlushContext.Create( - target, - pixelHandler.TextureFormat, - pixelHandler.PixelSizeInBytes, - configuration.MemoryAllocator, - compositionBatch.CompositionBounds); - - CompositionCoverageDefinition definition = compositionBatch.Definition; - if (TryPrepareGpuCoverage( - flushContext, - in definition, - out WebGPUFlushContext.CoverageEntry? coverageEntry, - out failure)) - { - gpuReady = true; - gpuSuccess = this.TryCompositeBatchTiled( - flushContext, - coverageEntry.GPUCoverageView, - compositionBatch.Commands, - compositionBatch.CompositionBounds, - blitToTarget: !useFlushSession || compositionBatch.IsFinalBatchInFlush, - out failure); - if (gpuSuccess) - { - if (useFlushSession && !compositionBatch.IsFinalBatchInFlush) - { - // Intermediate session batches defer final submit/readback until the last batch. - } - else - { - gpuSuccess = this.TryFinalizeFlush(flushContext, cpuRegion, compositionBatch.CompositionBounds); - } - } - } + gpuReady = true; + this.TestingPrepareCoverageCallCount += commandCount; + this.TestingReleaseCoverageCallCount += commandCount; + + gpuSuccess = this.TryRenderPreparedFlush( + flushContext, + preparedBatches, + configuration, + target.Bounds, + compositionBounds.Value, + out failure) && + this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); } catch (Exception ex) { failure = ex.Message; gpuSuccess = false; } - finally - { - if (!useFlushSession) - { - flushContext?.Dispose(); - } - } this.TestingGPUInitializationAttempted = true; this.TestingIsGPUReady = gpuReady; this.TestingLastGPUInitializationFailure = gpuSuccess ? null : failure; this.TestingLiveCoverageCount = 0; - if (useFlushSession) - { - if (gpuSuccess) - { - this.TestingGPUPrepareCoverageCallCount++; - this.TestingGPUCompositeCoverageCallCount += commandCount; - if (compositionBatch.IsFinalBatchInFlush) - { - WebGPUFlushContext.CompleteFlushSession(compositionBatch.FlushId); - } - - return; - } - - WebGPUFlushContext.CompleteFlushSession(compositionBatch.FlushId); - if (hadExistingSession) - { - throw new InvalidOperationException($"WebGPU flush session failed after prior GPU batches. Reason: {failure ?? "Unknown error"}"); - } - - this.TestingFallbackPrepareCoverageCallCount++; - this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); - return; - } - if (gpuSuccess) { - this.TestingGPUPrepareCoverageCallCount++; + this.TestingGPUPrepareCoverageCallCount += commandCount; this.TestingGPUCompositeCoverageCallCount += commandCount; return; } - this.TestingFallbackPrepareCoverageCallCount++; + this.TestingFallbackPrepareCoverageCallCount += commandCount; this.TestingFallbackCompositeCoverageCallCount += commandCount; - this.FlushCompositionsFallback(configuration, target, compositionBatch, hasCpuRegion); + this.FlushCompositionsFallback( + configuration, + target, + compositionScene, + hasCpuRegion, + compositionBounds); } /// - /// Checks whether all prepared commands in the batch are directly composable by WebGPU. + /// Checks whether all scene commands are directly composable by WebGPU. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) + private static bool AreAllCompositionBrushesSupported(IReadOnlyList commands) + where TPixel : unmanaged, IPixel { for (int i = 0; i < commands.Count; i++) { - if (!IsSupportedCompositionBrush(commands[i].Brush)) + Brush brush = commands[i].Brush; + if (!IsSupportedCompositionBrush(brush)) { return false; } @@ -410,66 +325,29 @@ private static bool AreAllCompositionBrushesSupported(IReadOnlyList brush is SolidBrush or ImageBrush; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetCompositionBounds(IReadOnlyList commands, out Rectangle bounds) - { - if (commands.Count == 0) - { - bounds = default; - return false; - } - - Rectangle union = commands[0].DestinationRegion; - for (int i = 1; i < commands.Count; i++) - { - union = Rectangle.Union(union, commands[i].DestinationRegion); - } - - bounds = union; - return union.Width > 0 && union.Height > 0; - } - - private static bool TryGetCompositionBounds(List batches, out Rectangle bounds) - { - bool hasBounds = false; - Rectangle union = default; - - for (int i = 0; i < batches.Count; i++) - { - if (!TryGetCompositionBounds(batches[i].Commands, out Rectangle batchBounds)) - { - continue; - } - - union = hasBounds ? Rectangle.Union(union, batchBounds) : batchBounds; - hasBounds = true; - } - - bounds = union; - return hasBounds; - } - /// - /// Executes one prepared batch on the CPU fallback backend. + /// Executes the scene on the CPU fallback backend. /// /// The destination pixel format. /// The active processing configuration. /// The original destination frame. - /// The prepared batch to execute. + /// The scene to execute. /// /// Indicates whether exposes CPU pixels directly. When , /// a temporary staging frame is composed and uploaded to the native surface. /// + /// The destination-local bounds touched by the scene when known. private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, - CompositionBatch compositionBatch, - bool hasCpuRegion) + CompositionScene compositionScene, + bool hasCpuRegion, + Rectangle? compositionBounds) where TPixel : unmanaged, IPixel { if (hasCpuRegion) { - this.fallbackBackend.FlushPreparedBatch(configuration, target, compositionBatch); + this.fallbackBackend.FlushCompositions(configuration, target, compositionScene); return; } @@ -479,10 +357,10 @@ private void FlushCompositionsFallback( Buffer2DRegion stagingRegion = stagingLease.Region; ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); - this.fallbackBackend.FlushPreparedBatch(configuration, stagingFrame, compositionBatch); + this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene); using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target, configuration.MemoryAllocator); - if (compositionBatch.CompositionBounds is Rectangle uploadBounds && + if (compositionBounds is Rectangle uploadBounds && uploadBounds.Width > 0 && uploadBounds.Height > 0) { @@ -508,24 +386,526 @@ private void FlushCompositionsFallback( } } - /// - /// Resolves (or creates) cached GPU coverage for the batch definition. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryPrepareGpuCoverage( + private bool TryRenderPreparedFlush( + WebGPUFlushContext flushContext, + List preparedBatches, + Configuration configuration, + Rectangle targetBounds, + Rectangle compositionBounds, + out string? error) + where TPixel : unmanaged, IPixel + { + Rectangle targetLocalBounds = Rectangle.Intersect( + new Rectangle(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height), + compositionBounds); + if (targetLocalBounds.Width <= 0 || targetLocalBounds.Height <= 0) + { + error = null; + return true; + } + + if (!flushContext.EnsureCommandEncoder()) + { + error = "Failed to create WebGPU command encoder."; + return false; + } + + if (flushContext.TargetTexture is null || flushContext.TargetView is null) + { + error = "WebGPU flush context does not expose required target resources."; + return false; + } + + WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; + nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; + bool hasValidDestinationBuffer = + destinationPixelsBuffer is not null && + destinationPixelsByteSize != 0 && + flushContext.CompositeDestinationWidth == targetLocalBounds.Width && + flushContext.CompositeDestinationHeight == targetLocalBounds.Height; + + TextureView* sourceTextureView = flushContext.TargetView; + int sourceOriginX = targetLocalBounds.X; + int sourceOriginY = targetLocalBounds.Y; + if (!flushContext.CanSampleTargetTexture) + { + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out Texture* sourceTexture, + out sourceTextureView, + out error)) + { + return false; + } + + CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); + sourceOriginX = 0; + sourceOriginY = 0; + } + + if (!hasValidDestinationBuffer) + { + if (!TryCreateDestinationPixelsBuffer( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out destinationPixelsBuffer, + out destinationPixelsByteSize, + out error)) + { + return false; + } + + flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; + flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; + flushContext.CompositeDestinationWidth = targetLocalBounds.Width; + flushContext.CompositeDestinationHeight = targetLocalBounds.Height; + } + + if (!TryInitializeDestinationPixels( + flushContext, + sourceTextureView, + destinationPixelsBuffer, + targetLocalBounds, + sourceOriginX, + sourceOriginY, + destinationPixelsByteSize, + out error)) + { + return false; + } + + List coverageDefinitions = new(); + Dictionary coverageDefinitionIndexByKey = new(); + List pendingCommands = new(); + for (int i = 0; i < preparedBatches.Count; i++) + { + CompositionBatch batch = preparedBatches[i]; + if (batch.Commands.Count == 0) + { + continue; + } + + int definitionKey = batch.Definition.DefinitionKey; + if (!coverageDefinitionIndexByKey.TryGetValue(definitionKey, out int coverageDefinitionIndex)) + { + coverageDefinitionIndex = coverageDefinitions.Count; + coverageDefinitions.Add(batch.Definition); + coverageDefinitionIndexByKey.Add(definitionKey, coverageDefinitionIndex); + } + + IReadOnlyList commands = batch.Commands; + for (int commandIndex = 0; commandIndex < commands.Count; commandIndex++) + { + pendingCommands.Add(new PreparedCompositePendingCommand(coverageDefinitionIndex, commands[commandIndex])); + } + + this.TestingComputePathBatchCount++; + } + + if (!this.TryCreateCoverageTextureFromFlattened( + flushContext, + coverageDefinitions, + configuration, + out TextureView* coverageView, + out CoveragePlacement[] coveragePlacements, + out error)) + { + return false; + } + + List compositeCommands = new(pendingCommands.Count); + for (int i = 0; i < pendingCommands.Count; i++) + { + PreparedCompositePendingCommand pending = pendingCommands[i]; + CoveragePlacement coveragePlacement = coveragePlacements[pending.CoverageDefinitionIndex]; + compositeCommands.Add(new PreparedCompositeWorkItem(pending.Command, coveragePlacement.OriginX, coveragePlacement.OriginY, (nint)coverageView)); + } + + if (!this.TryDispatchPreparedCompositeCommands( + flushContext, + sourceTextureView, + destinationPixelsBuffer, + destinationPixelsByteSize, + targetBounds, + targetLocalBounds, + compositeCommands, + out error)) + { + return false; + } + + if (!TryBlitDestinationPixelsToTarget( + flushContext, + destinationPixelsBuffer, + destinationPixelsByteSize, + targetLocalBounds, + out error)) + { + return false; + } + + error = null; + return true; + } + + private bool TryDispatchPreparedCompositeCommands( WebGPUFlushContext flushContext, - in CompositionCoverageDefinition definition, - [NotNullWhen(true)] out WebGPUFlushContext.CoverageEntry? coverageEntry, + TextureView* defaultBrushTextureView, + WgpuBuffer* destinationPixelsBuffer, + nuint destinationPixelsByteSize, + Rectangle targetBounds, + Rectangle targetLocalBounds, + IReadOnlyList compositeCommands, out string? error) + where TPixel : unmanaged, IPixel { - lock (flushContext.DeviceState.SyncRoot) + error = null; + if (compositeCommands.Count == 0) { - return flushContext.DeviceState.TryGetOrCreateCoverageEntry( - in definition, + return true; + } + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + "prepared-composite", + PreparedCompositeComputeShader.Code, + TryCreatePreparedCompositeBindGroupLayout, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + uint parameterSize = (uint)Unsafe.SizeOf(); + int parameterUploadByteCount = checked((int)(parameterSize * (uint)compositeCommands.Count)); + IMemoryOwner parametersUploadOwner = flushContext.MemoryAllocator.Allocate(parameterUploadByteCount); + try + { + Span parameterUpload = parametersUploadOwner.Memory.Span[..parameterUploadByteCount]; + parameterUpload.Clear(); + TextureView* sourceTextureView = defaultBrushTextureView; + nint sourceTextureViewHandle = (nint)defaultBrushTextureView; + bool hasImageTexture = false; + nint coverageTextureViewHandle = 0; + bool hasCoverageTexture = false; + uint validCommandCount = 0; + + for (int i = 0; i < compositeCommands.Count; i++) + { + PreparedCompositeWorkItem workItem = compositeCommands[i]; + PreparedCompositionCommand command = workItem.Command; + if (command.DestinationRegion.Width <= 0 || command.DestinationRegion.Height <= 0) + { + continue; + } + + uint brushType; + int brushOriginX = 0; + int brushOriginY = 0; + int brushRegionX = 0; + int brushRegionY = 0; + int brushRegionWidth = 1; + int brushRegionHeight = 1; + Vector4 solidColor = default; + + if (command.Brush is SolidBrush solidBrush) + { + brushType = PreparedBrushTypeSolid; + solidColor = solidBrush.Color.ToScaledVector4(); + } + else if (command.Brush is ImageBrush imageBrush) + { + brushType = PreparedBrushTypeImage; + Image image = (Image)imageBrush.SourceImage; + + if (!TryGetOrCreateImageTextureView( + flushContext, + image, + flushContext.TextureFormat, + out TextureView* brushTextureView, + out error)) + { + return false; + } + + if (!hasImageTexture) + { + sourceTextureView = brushTextureView; + sourceTextureViewHandle = (nint)brushTextureView; + hasImageTexture = true; + } + else if (sourceTextureViewHandle != (nint)brushTextureView) + { + error = "Prepared composite flush currently supports one image brush texture per dispatch."; + return false; + } + + Rectangle sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)imageBrush.SourceRegion); + brushRegionX = sourceRegion.X; + brushRegionY = sourceRegion.Y; + brushRegionWidth = sourceRegion.Width; + brushRegionHeight = sourceRegion.Height; + brushOriginX = command.BrushBounds.X + imageBrush.Offset.X - targetBounds.X - targetLocalBounds.X; + brushOriginY = command.BrushBounds.Y + imageBrush.Offset.Y - targetBounds.Y - targetLocalBounds.Y; + } + else + { + error = "Unsupported brush type."; + return false; + } + + if (!hasCoverageTexture) + { + coverageTextureViewHandle = workItem.CoverageTextureView; + hasCoverageTexture = true; + } + else if (coverageTextureViewHandle != workItem.CoverageTextureView) + { + error = "Prepared composite flush requires a shared coverage texture."; + return false; + } + + int destinationX = command.DestinationRegion.X - targetLocalBounds.X; + int destinationY = command.DestinationRegion.Y - targetLocalBounds.Y; + PreparedCompositeParameters parameters = new( + destinationX, + destinationY, + command.DestinationRegion.Width, + command.DestinationRegion.Height, + command.SourceOffset.X + workItem.CoverageOriginX, + command.SourceOffset.Y + workItem.CoverageOriginY, + targetLocalBounds.Width, + brushType, + brushOriginX, + brushOriginY, + brushRegionX, + brushRegionY, + brushRegionWidth, + brushRegionHeight, + (uint)command.GraphicsOptions.ColorBlendingMode, + (uint)command.GraphicsOptions.AlphaCompositionMode, + command.GraphicsOptions.BlendPercentage, + solidColor); + + int parameterOffset = checked((int)(validCommandCount * parameterSize)); + MemoryMarshal.Write( + parameterUpload.Slice(parameterOffset, (int)parameterSize), + in parameters); + + validCommandCount++; + } + + if (validCommandCount == 0) + { + error = null; + return true; + } + + int usedParameterByteCount = checked((int)(validCommandCount * parameterSize)); + BufferDescriptor paramsDescriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = (nuint)usedParameterByteCount + }; + + WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); + if (paramsBuffer is null) + { + error = "Failed to create composite parameter buffer."; + return false; + } + + flushContext.TrackBuffer(paramsBuffer); + fixed (byte* parameterUploadPtr = parameterUpload) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + paramsBuffer, + 0, + parameterUploadPtr, + (nuint)usedParameterByteCount); + } + + BufferDescriptor dispatchConfigDescriptor = new() + { + Usage = BufferUsage.Uniform | BufferUsage.CopyDst, + Size = (nuint)Unsafe.SizeOf() + }; + + WgpuBuffer* dispatchConfigBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in dispatchConfigDescriptor); + if (dispatchConfigBuffer is null) + { + error = "Failed to create composite dispatch config buffer."; + return false; + } + + flushContext.TrackBuffer(dispatchConfigBuffer); + PreparedCompositeDispatchConfig dispatchConfig = new( + validCommandCount, + (uint)targetLocalBounds.Width, + (uint)targetLocalBounds.Height); + flushContext.Api.QueueWriteBuffer( flushContext.Queue, - out coverageEntry, - out error); + dispatchConfigBuffer, + 0, + &dispatchConfig, + (nuint)Unsafe.SizeOf()); + + if (!hasCoverageTexture) + { + error = "Prepared composite flush did not produce a coverage texture."; + return false; + } + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[5]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + TextureView = (TextureView*)coverageTextureViewHandle + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + TextureView = sourceTextureView + }; + bindGroupEntries[2] = new BindGroupEntry + { + Binding = 2, + Buffer = destinationPixelsBuffer, + Offset = 0, + Size = destinationPixelsByteSize + }; + bindGroupEntries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = paramsBuffer, + Offset = 0, + Size = (nuint)usedParameterByteCount + }; + bindGroupEntries[4] = new BindGroupEntry + { + Binding = 4, + Buffer = dispatchConfigBuffer, + Offset = 0, + Size = (nuint)Unsafe.SizeOf() + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 5, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create prepared composite bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin prepared composite compute pass."; + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups( + passEncoder, + DivideRoundUp(targetLocalBounds.Width, CompositeComputeWorkgroupSize), + DivideRoundUp(targetLocalBounds.Height, CompositeComputeWorkgroupSize), + 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + } + finally + { + parametersUploadOwner.Dispose(); + } + + error = null; + return true; + } + + private static bool TryGetOrCreateImageTextureView( + WebGPUFlushContext flushContext, + Image image, + TextureFormat textureFormat, + out TextureView* textureView, + out string? error) + where TPixel : unmanaged, IPixel + { + if (flushContext.TryGetCachedSourceTextureView(image, out textureView)) + { + error = null; + return true; + } + + TextureDescriptor descriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)image.Width, (uint)image.Height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); + if (texture is null) + { + textureView = null; + error = "Failed to create image texture."; + return false; + } + + TextureViewDescriptor viewDescriptor = new() + { + Format = descriptor.Format, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in viewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + error = "Failed to create image texture view."; + return false; } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + flushContext.CacheSourceTextureView(image, textureView); + + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + WebGPUFlushContext.UploadTextureFromRegion( + flushContext.Api, + flushContext.Queue, + texture, + region, + flushContext.MemoryAllocator); + + error = null; + return true; } /// @@ -559,7 +939,7 @@ private static bool TryCreateDestinationPixelsBuffer( } /// - /// Initializes destination storage from the current destination texture contents. + /// Initializes destination storage from the current destination texture contents in premultiplied form. /// private static bool TryInitializeDestinationPixels( WebGPUFlushContext flushContext, @@ -671,7 +1051,7 @@ private static bool TryInitializeDestinationPixels( } /// - /// Writes composed destination storage back to the render target through a fullscreen blit. + /// Writes composed premultiplied destination storage back to the render target through a fullscreen blit. /// private static bool TryBlitDestinationPixelsToTarget( WebGPUFlushContext flushContext, @@ -749,7 +1129,7 @@ private static bool TryBlitDestinationPixelsToTarget( } flushContext.TrackBindGroup(bindGroup); - if (!flushContext.BeginRenderPass(flushContext.TargetView)) + if (!flushContext.BeginRenderPass(flushContext.TargetView, loadExisting: true)) { error = "Failed to begin destination blit render pass."; return false; @@ -757,6 +1137,14 @@ private static bool TryBlitDestinationPixelsToTarget( flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); + flushContext.Api.RenderPassEncoderSetViewport( + flushContext.PassEncoder, + 0, + 0, + flushContext.TargetBounds.Width, + flushContext.TargetBounds.Height, + 0, + 1); flushContext.Api.RenderPassEncoderSetScissorRect( flushContext.PassEncoder, (uint)destinationBounds.X, @@ -880,6 +1268,89 @@ private static bool TryCreateDestinationBlitBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by prepared composite compute shader. + /// + private static bool TryCreatePreparedCompositeBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.UnfilterableFloat, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 5, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite bind group layout."; + return false; + } + + error = null; + return true; + } + /// /// Creates one transient composition texture that can be rendered to, sampled from, and copied. /// @@ -974,6 +1445,13 @@ private static void CopyTextureRegion( private static uint DivideRoundUp(int value, int divisor) => (uint)((value + divisor - 1) / divisor); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FloatToUInt32Bits(float value) + => unchecked((uint)System.BitConverter.SingleToInt32Bits(value)); + /// /// Finalizes one flush by submitting command buffers and optionally reading results back to CPU memory. /// @@ -1273,37 +1751,187 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv /// Destination initialization parameters consumed by . /// [StructLayout(LayoutKind.Sequential)] - private readonly struct CompositeDestinationInitParameters( - int batchWidth, - int batchHeight, - int sourceOriginX, - int sourceOriginY) + private readonly struct CompositeDestinationInitParameters { - public readonly int BatchWidth = batchWidth; - - public readonly int BatchHeight = batchHeight; - - public readonly int SourceOriginX = sourceOriginX; - - public readonly int SourceOriginY = sourceOriginY; + public readonly int BatchWidth; + public readonly int BatchHeight; + public readonly int SourceOriginX; + public readonly int SourceOriginY; + + public CompositeDestinationInitParameters( + int batchWidth, + int batchHeight, + int sourceOriginX, + int sourceOriginY) + { + this.BatchWidth = batchWidth; + this.BatchHeight = batchHeight; + this.SourceOriginX = sourceOriginX; + this.SourceOriginY = sourceOriginY; + } } /// /// Destination blit parameters consumed by . /// [StructLayout(LayoutKind.Sequential)] - private readonly struct CompositeDestinationBlitParameters( - int batchWidth, - int batchHeight, - int targetOriginX, - int targetOriginY) + private readonly struct CompositeDestinationBlitParameters { - public readonly int BatchWidth = batchWidth; + public readonly int BatchWidth; + public readonly int BatchHeight; + public readonly int TargetOriginX; + public readonly int TargetOriginY; + + public CompositeDestinationBlitParameters( + int batchWidth, + int batchHeight, + int targetOriginX, + int targetOriginY) + { + this.BatchWidth = batchWidth; + this.BatchHeight = batchHeight; + this.TargetOriginX = targetOriginX; + this.TargetOriginY = targetOriginY; + } + } - public readonly int BatchHeight = batchHeight; + private readonly struct PreparedCompositeWorkItem + { + public PreparedCompositeWorkItem(in PreparedCompositionCommand command, int coverageOriginX, int coverageOriginY, nint coverageTextureView) + { + this.Command = command; + this.CoverageOriginX = coverageOriginX; + this.CoverageOriginY = coverageOriginY; + this.CoverageTextureView = coverageTextureView; + } - public readonly int TargetOriginX = targetOriginX; + public PreparedCompositionCommand Command { get; } - public readonly int TargetOriginY = targetOriginY; + public int CoverageOriginX { get; } + + public int CoverageOriginY { get; } + + public nint CoverageTextureView { get; } + } + + private readonly struct PreparedCompositePendingCommand + { + public PreparedCompositePendingCommand(int coverageDefinitionIndex, in PreparedCompositionCommand command) + { + this.CoverageDefinitionIndex = coverageDefinitionIndex; + this.Command = command; + } + + public int CoverageDefinitionIndex { get; } + + public PreparedCompositionCommand Command { get; } + } + + private readonly struct CoveragePlacement + { + public CoveragePlacement(int originX, int originY, int width, int height) + { + this.OriginX = originX; + this.OriginY = originY; + this.Width = width; + this.Height = height; + } + + public int OriginX { get; } + + public int OriginY { get; } + + public int Width { get; } + + public int Height { get; } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeDispatchConfig + { + public readonly uint CommandCount; + public readonly uint TargetWidth; + public readonly uint TargetHeight; + public readonly uint Pad0; + + public PreparedCompositeDispatchConfig(uint commandCount, uint targetWidth, uint targetHeight) + { + this.CommandCount = commandCount; + this.TargetWidth = targetWidth; + this.TargetHeight = targetHeight; + this.Pad0 = 0; + } + } + + /// + /// Prepared composite command parameters consumed by . + /// + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeParameters + { + public readonly uint DestinationX; + public readonly uint DestinationY; + public readonly uint DestinationWidth; + public readonly uint DestinationHeight; + public readonly uint CoverageOffsetX; + public readonly uint CoverageOffsetY; + public readonly uint TargetWidth; + public readonly uint BrushType; + public readonly uint BrushOriginX; + public readonly uint BrushOriginY; + public readonly uint BrushRegionX; + public readonly uint BrushRegionY; + public readonly uint BrushRegionWidth; + public readonly uint BrushRegionHeight; + public readonly uint ColorBlendMode; + public readonly uint AlphaCompositionMode; + public readonly uint BlendPercentage; + public readonly uint SolidR; + public readonly uint SolidG; + public readonly uint SolidB; + public readonly uint SolidA; + + public PreparedCompositeParameters( + int destinationX, + int destinationY, + int destinationWidth, + int destinationHeight, + int coverageOffsetX, + int coverageOffsetY, + int targetWidth, + uint brushType, + int brushOriginX, + int brushOriginY, + int brushRegionX, + int brushRegionY, + int brushRegionWidth, + int brushRegionHeight, + uint colorBlendMode, + uint alphaCompositionMode, + float blendPercentage, + Vector4 solidColor) + { + this.DestinationX = (uint)destinationX; + this.DestinationY = (uint)destinationY; + this.DestinationWidth = (uint)destinationWidth; + this.DestinationHeight = (uint)destinationHeight; + this.CoverageOffsetX = (uint)coverageOffsetX; + this.CoverageOffsetY = (uint)coverageOffsetY; + this.TargetWidth = (uint)targetWidth; + this.BrushType = brushType; + this.BrushOriginX = (uint)brushOriginX; + this.BrushOriginY = (uint)brushOriginY; + this.BrushRegionX = (uint)brushRegionX; + this.BrushRegionY = (uint)brushRegionY; + this.BrushRegionWidth = (uint)brushRegionWidth; + this.BrushRegionHeight = (uint)brushRegionHeight; + this.ColorBlendMode = colorBlendMode; + this.AlphaCompositionMode = alphaCompositionMode; + this.BlendPercentage = FloatToUInt32Bits(blendPercentage); + this.SolidR = FloatToUInt32Bits(solidColor.X); + this.SolidG = FloatToUInt32Bits(solidColor.Y); + this.SolidB = FloatToUInt32Bits(solidColor.Z); + this.SolidA = FloatToUInt32Bits(solidColor.W); + } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 8f240c5de..7016ea068 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -3,11 +3,9 @@ using System.Buffers; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -26,7 +24,6 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable { private static readonly ConcurrentDictionary FallbackStagingCache = new(); private static readonly ConcurrentDictionary DeviceStateCache = new(); - private static readonly ConcurrentDictionary FlushSessionContexts = new(); private static readonly object SharedHandleSync = new(); private const int CallbackTimeoutMilliseconds = 10_000; @@ -238,13 +235,6 @@ public static void ClearFallbackStagingCache() public static void ClearDeviceStateCache() { - foreach (WebGPUFlushContext context in FlushSessionContexts.Values) - { - context.Dispose(); - } - - FlushSessionContexts.Clear(); - foreach (DeviceSharedState state in DeviceStateCache.Values) { state.Dispose(); @@ -253,42 +243,6 @@ public static void ClearDeviceStateCache() DeviceStateCache.Clear(); } - public static WebGPUFlushContext GetOrCreateFlushSessionContext( - int flushId, - ICanvasFrame frame, - TextureFormat expectedTextureFormat, - int pixelSizeInBytes, - MemoryAllocator memoryAllocator, - Rectangle? initialUploadBounds, - out bool fromCache) - where TPixel : unmanaged, IPixel - { - if (FlushSessionContexts.TryGetValue(flushId, out WebGPUFlushContext? cached)) - { - fromCache = true; - return cached; - } - - fromCache = false; - WebGPUFlushContext created = Create(frame, expectedTextureFormat, pixelSizeInBytes, memoryAllocator, initialUploadBounds); - if (FlushSessionContexts.TryAdd(flushId, created)) - { - return created; - } - - created.Dispose(); - fromCache = true; - return FlushSessionContexts[flushId]; - } - - public static void CompleteFlushSession(int flushId) - { - if (FlushSessionContexts.TryRemove(flushId, out WebGPUFlushContext? context)) - { - context.Dispose(); - } - } - public static bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle, out string? error) { if (WebGPURuntime.TryGetSharedHandles(out Device* sharedDevice, out Queue* sharedQueue)) @@ -359,6 +313,12 @@ public bool EnsureCommandEncoder() /// Begins a render pass that targets the specified texture view. /// public bool BeginRenderPass(TextureView* targetView) + => this.BeginRenderPass(targetView, loadExisting: false); + + /// + /// Begins a render pass that targets the specified texture view, optionally preserving existing contents. + /// + public bool BeginRenderPass(TextureView* targetView, bool loadExisting) { if (this.PassEncoder is not null) { @@ -374,7 +334,7 @@ public bool BeginRenderPass(TextureView* targetView) { View = targetView, ResolveTarget = null, - LoadOp = LoadOp.Load, + LoadOp = loadExisting ? LoadOp.Load : LoadOp.Clear, StoreOp = StoreOp.Store, ClearValue = default }; @@ -442,6 +402,21 @@ public void TrackTexture(Texture* texture) } } + public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureView) + { + if (this.cachedSourceTextureViews.TryGetValue(image, out nint handle) && handle != 0) + { + textureView = (TextureView*)handle; + return true; + } + + textureView = null; + return false; + } + + public void CacheSourceTextureView(Image image, TextureView* textureView) + => this.cachedSourceTextureViews[image] = (nint)textureView; + public void Dispose() { if (this.disposed) @@ -864,17 +839,17 @@ internal static void UploadTextureFromRegion( }; Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + int rowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + uint alignedRowBytes = AlignTo256((uint)rowBytes); if (sourceRegion.Buffer.MemoryGroup.Count == 1) { int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); - int directPathRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + directPathRowBytes; - long directPathPackedByteCount = (long)directPathRowBytes * sourceRegion.Height; + long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + rowBytes; + long packedByteCountEstimate = (long)alignedRowBytes * sourceRegion.Height; - // For contiguous backing memory, avoid row packing unless the region is very sparse. - // This keeps the hot path allocation-free for common text and image workloads. - if (directByteCount <= directPathPackedByteCount * 2) + // Only use the direct path when the stride satisfies WebGPU's alignment requirement. + if ((uint)sourceStrideBytes == alignedRowBytes && directByteCount <= packedByteCountEstimate * 2) { int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.Width) + sourceRegion.Rectangle.X); int startByteOffset = checked(startPixelIndex * pixelSizeInBytes); @@ -899,20 +874,21 @@ internal static void UploadTextureFromRegion( } } - int packedRowBytes = checked(sourceRegion.Width * pixelSizeInBytes); - int packedByteCount = checked(packedRowBytes * sourceRegion.Height); + int alignedRowBytesInt = checked((int)alignedRowBytes); + int packedByteCount = checked(alignedRowBytesInt * sourceRegion.Height); using IMemoryOwner packedOwner = memoryAllocator.Allocate(packedByteCount); Span packedData = packedOwner.Memory.Span[..packedByteCount]; + packedData.Clear(); for (int y = 0; y < sourceRegion.Height; y++) { ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); - MemoryMarshal.AsBytes(sourceRow).CopyTo(packedData.Slice(y * packedRowBytes, packedRowBytes)); + MemoryMarshal.AsBytes(sourceRow).Slice(0, rowBytes).CopyTo(packedData.Slice(y * alignedRowBytesInt, rowBytes)); } TextureDataLayout packedLayout = new() { Offset = 0, - BytesPerRow = (uint)packedRowBytes, + BytesPerRow = alignedRowBytes, RowsPerImage = (uint)sourceRegion.Height }; @@ -927,11 +903,10 @@ internal static void UploadTextureFromRegion( internal sealed class DeviceSharedState : IDisposable { - private readonly Dictionary coverageCache = []; private readonly ConcurrentDictionary cpuTargetCache = new(); private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); - private WebGPURasterizer? coverageRasterizer; + private readonly ConcurrentDictionary sharedBuffers = new(StringComparer.Ordinal); private bool disposed; internal DeviceSharedState(WebGPU api, Device* device) @@ -952,8 +927,6 @@ internal DeviceSharedState(WebGPU api, Device* device) public Device* Device { get; } - public int CoverageCount => this.coverageCache.Count; - public CpuTargetLease RentCpuTarget( TextureFormat textureFormat, int width, @@ -965,69 +938,6 @@ public CpuTargetLease RentCpuTarget( return entry.Rent(this.Api, this.Device, in key); } - public bool TryEnsureCoverageResources(out string? error) - { - if (this.disposed) - { - error = "WebGPU device state is disposed."; - return false; - } - - this.coverageRasterizer ??= new WebGPURasterizer(this.Api); - if (!this.coverageRasterizer.IsInitialized && !this.coverageRasterizer.Initialize(this.Device)) - { - error = "Failed to initialize WebGPU coverage rasterizer."; - return false; - } - - error = null; - return true; - } - - public bool TryGetOrCreateCoverageEntry( - in CompositionCoverageDefinition definition, - Queue* queue, - [NotNullWhen(true)] out CoverageEntry? coverageEntry, - out string? error) - { - if (!this.TryEnsureCoverageResources(out error)) - { - coverageEntry = null; - return false; - } - - if (this.coverageCache.TryGetValue(definition.DefinitionKey, out CoverageEntry? cached)) - { - coverageEntry = cached; - return true; - } - - RasterizerOptions rasterizerOptions = definition.RasterizerOptions; - if (this.coverageRasterizer is null || - !this.coverageRasterizer.TryCreateCoverageTexture( - definition.Path, - in rasterizerOptions, - this.Device, - queue, - out Texture* coverageTexture, - out TextureView* coverageView)) - { - coverageEntry = null; - error = "Failed to rasterize coverage texture."; - return false; - } - - Size size = rasterizerOptions.Interest.Size; - coverageEntry = new CoverageEntry(size.Width, size.Height) - { - GPUCoverageTexture = coverageTexture, - GPUCoverageView = coverageView - }; - this.coverageCache.Add(definition.DefinitionKey, coverageEntry); - error = null; - return true; - } - public bool TryGetOrCreateCompositePipeline( string pipelineKey, ReadOnlySpan shaderCode, @@ -1191,22 +1101,86 @@ infrastructure.PipelineLayout is null || } } - public void Dispose() + public bool TryGetOrCreateSharedBuffer( + string bufferKey, + BufferUsage usage, + nuint requiredSize, + out WgpuBuffer* buffer, + out nuint capacity, + out string? error) { + buffer = null; + capacity = 0; + if (this.disposed) { - return; + error = "WebGPU device state is disposed."; + return false; + } + + if (string.IsNullOrWhiteSpace(bufferKey)) + { + error = "Shared buffer key cannot be empty."; + return false; } - foreach (CoverageEntry entry in this.coverageCache.Values) + if (requiredSize == 0) { - ReleaseCoverageTexture(this.Api, entry); + error = $"Shared buffer '{bufferKey}' requires a non-zero size."; + return false; } - this.coverageCache.Clear(); + SharedBufferInfrastructure infrastructure = this.sharedBuffers.GetOrAdd( + bufferKey, + static _ => new SharedBufferInfrastructure()); + lock (infrastructure) + { + if (infrastructure.Buffer is not null && + infrastructure.Capacity >= requiredSize && + infrastructure.Usage == usage) + { + buffer = infrastructure.Buffer; + capacity = infrastructure.Capacity; + error = null; + return true; + } + + if (infrastructure.Buffer is not null) + { + this.Api.BufferRelease(infrastructure.Buffer); + infrastructure.Buffer = null; + infrastructure.Capacity = 0; + } + + BufferDescriptor descriptor = new() + { + Usage = usage, + Size = requiredSize + }; + + WgpuBuffer* createdBuffer = this.Api.DeviceCreateBuffer(this.Device, in descriptor); + if (createdBuffer is null) + { + error = $"Failed to create shared buffer '{bufferKey}'."; + return false; + } + + infrastructure.Buffer = createdBuffer; + infrastructure.Capacity = requiredSize; + infrastructure.Usage = usage; + buffer = createdBuffer; + capacity = requiredSize; + error = null; + return true; + } + } - this.coverageRasterizer?.Release(); - this.coverageRasterizer = null; + public void Dispose() + { + if (this.disposed) + { + return; + } foreach (CompositePipelineInfrastructure infrastructure in this.compositePipelines.Values) { @@ -1222,13 +1196,27 @@ public void Dispose() this.compositeComputePipelines.Clear(); + foreach (SharedBufferInfrastructure infrastructure in this.sharedBuffers.Values) + { + lock (infrastructure) + { + if (infrastructure.Buffer is not null) + { + this.Api.BufferRelease(infrastructure.Buffer); + infrastructure.Buffer = null; + infrastructure.Capacity = 0; + } + } + } + + this.sharedBuffers.Clear(); + foreach (CpuTargetEntry entry in this.cpuTargetCache.Values) { entry.Dispose(this.Api); } this.cpuTargetCache.Clear(); - this.disposed = true; } @@ -1463,21 +1451,6 @@ private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfra } } - private static void ReleaseCoverageTexture(WebGPU api, CoverageEntry entry) - { - if (entry.GPUCoverageView is not null) - { - api.TextureViewRelease(entry.GPUCoverageView); - entry.GPUCoverageView = null; - } - - if (entry.GPUCoverageTexture is not null) - { - api.TextureRelease(entry.GPUCoverageTexture); - entry.GPUCoverageTexture = null; - } - } - internal readonly struct CpuTargetCacheKey( TextureFormat textureFormat, int width, @@ -1788,23 +1761,15 @@ private sealed class CompositeComputePipelineInfrastructure public ComputePipeline* Pipeline { get; set; } } - } - internal sealed class CoverageEntry - { - public CoverageEntry(int width, int height) + private sealed class SharedBufferInfrastructure { - this.Width = width; - this.Height = height; - } - - public int Width { get; } + public WgpuBuffer* Buffer { get; set; } - public int Height { get; } + public nuint Capacity { get; set; } - public Texture* GPUCoverageTexture { get; set; } - - public TextureView* GPUCoverageView { get; set; } + public BufferUsage Usage { get; set; } + } } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs deleted file mode 100644 index 5c496c129..000000000 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURasterizer.cs +++ /dev/null @@ -1,1064 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Owns WebGPU coverage rasterization resources and converts vector paths into reusable -/// coverage textures using a stencil-and-cover render pass. -/// -internal sealed unsafe class WebGPURasterizer -{ - private const uint CoverageCoverVertexCount = 3; - private const uint CoverageSampleCount = 4; - - private readonly WebGPU webGPU; - - private PipelineLayout* coveragePipelineLayout; - private RenderPipeline* coverageStencilEvenOddPipeline; - private RenderPipeline* coverageStencilNonZeroIncrementPipeline; - private RenderPipeline* coverageStencilNonZeroDecrementPipeline; - private RenderPipeline* coverageCoverPipeline; - private Texture* coverageScratchMultisampleTexture; - private TextureView* coverageScratchMultisampleView; - private Texture* coverageScratchStencilTexture; - private TextureView* coverageScratchStencilView; - private int coverageScratchWidth; - private int coverageScratchHeight; - private WgpuBuffer* coverageScratchVertexBuffer; - private ulong coverageScratchVertexCapacityBytes; - - public WebGPURasterizer(WebGPU webGPU) => this.webGPU = webGPU; - - private static ReadOnlySpan CoverageStencilVertexEntryPoint => "vs_edge\0"u8; - - private static ReadOnlySpan CoverageStencilFragmentEntryPoint => "fs_stencil\0"u8; - - private static ReadOnlySpan CoverageCoverVertexEntryPoint => "vs_cover\0"u8; - - private static ReadOnlySpan CoverageCoverFragmentEntryPoint => "fs_cover\0"u8; - - public bool IsInitialized => - this.coveragePipelineLayout is not null && - this.coverageStencilEvenOddPipeline is not null && - this.coverageStencilNonZeroIncrementPipeline is not null && - this.coverageStencilNonZeroDecrementPipeline is not null && - this.coverageCoverPipeline is not null; - - public bool Initialize(Device* device) - { - if (this.IsInitialized) - { - return true; - } - - return this.TryCreateCoveragePipelineLocked(device); - } - - public bool TryCreateCoverageTexture( - IPath path, - in RasterizerOptions rasterizerOptions, - Device* device, - Queue* queue, - out Texture* coverageTexture, - out TextureView* coverageView) - { - coverageTexture = null; - coverageView = null; - - if (!this.IsInitialized) - { - return false; - } - - if (!TryBuildCoverageTriangles( - path, - rasterizerOptions.Interest.Location, - rasterizerOptions.Interest.Size, - rasterizerOptions.SamplingOrigin, - out CoverageTriangleData coverageTriangleData)) - { - return false; - } - - return this.TryRasterizeCoverageTextureLocked( - in coverageTriangleData, - in rasterizerOptions, - device, - queue, - out coverageTexture, - out coverageView); - } - - public void Release() - { - this.ReleaseCoverageScratchResourcesLocked(); - - if (this.coverageCoverPipeline is not null) - { - this.webGPU.RenderPipelineRelease(this.coverageCoverPipeline); - this.coverageCoverPipeline = null; - } - - if (this.coverageStencilNonZeroDecrementPipeline is not null) - { - this.webGPU.RenderPipelineRelease(this.coverageStencilNonZeroDecrementPipeline); - this.coverageStencilNonZeroDecrementPipeline = null; - } - - if (this.coverageStencilNonZeroIncrementPipeline is not null) - { - this.webGPU.RenderPipelineRelease(this.coverageStencilNonZeroIncrementPipeline); - this.coverageStencilNonZeroIncrementPipeline = null; - } - - if (this.coverageStencilEvenOddPipeline is not null) - { - this.webGPU.RenderPipelineRelease(this.coverageStencilEvenOddPipeline); - this.coverageStencilEvenOddPipeline = null; - } - - if (this.coveragePipelineLayout is not null) - { - this.webGPU.PipelineLayoutRelease(this.coveragePipelineLayout); - this.coveragePipelineLayout = null; - } - } - - /// - /// Creates the render pipeline used for coverage rasterization. - /// - private bool TryCreateCoveragePipelineLocked(Device* device) - { - PipelineLayoutDescriptor pipelineLayoutDescriptor = new() - { - BindGroupLayoutCount = 0, - BindGroupLayouts = null - }; - - this.coveragePipelineLayout = this.webGPU.DeviceCreatePipelineLayout(device, in pipelineLayoutDescriptor); - if (this.coveragePipelineLayout is null) - { - return false; - } - - ShaderModule* shaderModule = null; - try - { - ReadOnlySpan shaderCode = CoverageRasterizationShader.Code; - fixed (byte* shaderCodePtr = shaderCode) - { - ShaderModuleWGSLDescriptor wgslDescriptor = new() - { - Chain = new ChainedStruct - { - SType = SType.ShaderModuleWgslDescriptor - }, - Code = shaderCodePtr - }; - - ShaderModuleDescriptor shaderDescriptor = new() - { - NextInChain = (ChainedStruct*)&wgslDescriptor - }; - - shaderModule = this.webGPU.DeviceCreateShaderModule(device, in shaderDescriptor); - } - - if (shaderModule is null) - { - return false; - } - - ReadOnlySpan stencilVertexEntryPoint = CoverageStencilVertexEntryPoint; - ReadOnlySpan stencilFragmentEntryPoint = CoverageStencilFragmentEntryPoint; - ReadOnlySpan coverVertexEntryPoint = CoverageCoverVertexEntryPoint; - ReadOnlySpan coverFragmentEntryPoint = CoverageCoverFragmentEntryPoint; - fixed (byte* stencilVertexEntryPointPtr = stencilVertexEntryPoint) - { - fixed (byte* stencilFragmentEntryPointPtr = stencilFragmentEntryPoint) - { - VertexAttribute* stencilVertexAttributes = stackalloc VertexAttribute[1]; - stencilVertexAttributes[0] = new VertexAttribute - { - Format = VertexFormat.Float32x2, - Offset = 0, - ShaderLocation = 0 - }; - - VertexBufferLayout* stencilVertexBuffers = stackalloc VertexBufferLayout[1]; - stencilVertexBuffers[0] = new VertexBufferLayout - { - ArrayStride = (ulong)Unsafe.SizeOf(), - StepMode = VertexStepMode.Vertex, - AttributeCount = 1, - Attributes = stencilVertexAttributes - }; - - VertexState stencilVertexState = new() - { - Module = shaderModule, - EntryPoint = stencilVertexEntryPointPtr, - BufferCount = 1, - Buffers = stencilVertexBuffers - }; - - ColorTargetState* stencilColorTargets = stackalloc ColorTargetState[1]; - stencilColorTargets[0] = new ColorTargetState - { - Format = TextureFormat.R8Unorm, - Blend = null, - WriteMask = ColorWriteMask.None - }; - - FragmentState stencilFragmentState = new() - { - Module = shaderModule, - EntryPoint = stencilFragmentEntryPointPtr, - TargetCount = 1, - Targets = stencilColorTargets - }; - - PrimitiveState primitiveState = new() - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }; - - MultisampleState multisampleState = new() - { - Count = CoverageSampleCount, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }; - - StencilFaceState evenOddStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.Invert - }; - - DepthStencilState evenOddDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = evenOddStencilFace, - StencilBack = evenOddStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor evenOddPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &evenOddDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilEvenOddPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in evenOddPipelineDescriptor); - if (this.coverageStencilEvenOddPipeline is null) - { - return false; - } - - StencilFaceState incrementStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.IncrementWrap - }; - - DepthStencilState incrementDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = incrementStencilFace, - StencilBack = incrementStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor incrementPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &incrementDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilNonZeroIncrementPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in incrementPipelineDescriptor); - if (this.coverageStencilNonZeroIncrementPipeline is null) - { - return false; - } - - StencilFaceState decrementStencilFace = new() - { - Compare = CompareFunction.Always, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.DecrementWrap - }; - - DepthStencilState decrementDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = decrementStencilFace, - StencilBack = decrementStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = uint.MaxValue, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor decrementPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = stencilVertexState, - Primitive = primitiveState, - DepthStencil = &decrementDepthStencilState, - Multisample = multisampleState, - Fragment = &stencilFragmentState - }; - - this.coverageStencilNonZeroDecrementPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in decrementPipelineDescriptor); - if (this.coverageStencilNonZeroDecrementPipeline is null) - { - return false; - } - } - } - - fixed (byte* coverVertexEntryPointPtr = coverVertexEntryPoint) - { - fixed (byte* coverFragmentEntryPointPtr = coverFragmentEntryPoint) - { - VertexState coverVertexState = new() - { - Module = shaderModule, - EntryPoint = coverVertexEntryPointPtr, - BufferCount = 0, - Buffers = null - }; - - ColorTargetState* coverColorTargets = stackalloc ColorTargetState[1]; - coverColorTargets[0] = new ColorTargetState - { - Format = TextureFormat.R8Unorm, - Blend = null, - WriteMask = ColorWriteMask.Red - }; - - FragmentState coverFragmentState = new() - { - Module = shaderModule, - EntryPoint = coverFragmentEntryPointPtr, - TargetCount = 1, - Targets = coverColorTargets - }; - - StencilFaceState coverStencilFace = new() - { - Compare = CompareFunction.NotEqual, - FailOp = StencilOperation.Keep, - DepthFailOp = StencilOperation.Keep, - PassOp = StencilOperation.Keep - }; - - DepthStencilState coverDepthStencilState = new() - { - Format = TextureFormat.Depth24PlusStencil8, - DepthWriteEnabled = false, - DepthCompare = CompareFunction.Always, - StencilFront = coverStencilFace, - StencilBack = coverStencilFace, - StencilReadMask = uint.MaxValue, - StencilWriteMask = 0, - DepthBias = 0, - DepthBiasSlopeScale = 0F, - DepthBiasClamp = 0F - }; - - RenderPipelineDescriptor coverPipelineDescriptor = new() - { - Layout = this.coveragePipelineLayout, - Vertex = coverVertexState, - Primitive = new PrimitiveState - { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None - }, - DepthStencil = &coverDepthStencilState, - Multisample = new MultisampleState - { - Count = CoverageSampleCount, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false - }, - Fragment = &coverFragmentState - }; - - this.coverageCoverPipeline = this.webGPU.DeviceCreateRenderPipeline(device, in coverPipelineDescriptor); - } - } - - return this.coverageCoverPipeline is not null; - } - finally - { - if (shaderModule is not null) - { - this.webGPU.ShaderModuleRelease(shaderModule); - } - } - } - - private bool TryEnsureCoverageScratchTargetsLocked( - Device* device, - int width, - int height, - out TextureView* multisampleCoverageView, - out TextureView* stencilView) - { - multisampleCoverageView = null; - stencilView = null; - - if (this.coverageScratchMultisampleView is not null && - this.coverageScratchStencilView is not null && - this.coverageScratchWidth == width && - this.coverageScratchHeight == height) - { - multisampleCoverageView = this.coverageScratchMultisampleView; - stencilView = this.coverageScratchStencilView; - return true; - } - - this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); - this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); - this.ReleaseTextureViewLocked(this.coverageScratchStencilView); - this.ReleaseTextureLocked(this.coverageScratchStencilTexture); - this.coverageScratchMultisampleView = null; - this.coverageScratchMultisampleTexture = null; - this.coverageScratchStencilView = null; - this.coverageScratchStencilTexture = null; - this.coverageScratchWidth = 0; - this.coverageScratchHeight = 0; - - TextureDescriptor multisampleCoverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - Texture* createdMultisampleCoverageTexture = - this.webGPU.DeviceCreateTexture(device, in multisampleCoverageTextureDescriptor); - if (createdMultisampleCoverageTexture is null) - { - return false; - } - - TextureViewDescriptor coverageViewDescriptor = new() - { - Format = TextureFormat.R8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* createdMultisampleCoverageView = this.webGPU.TextureCreateView(createdMultisampleCoverageTexture, in coverageViewDescriptor); - if (createdMultisampleCoverageView is null) - { - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - TextureDescriptor stencilTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.Depth24PlusStencil8, - MipLevelCount = 1, - SampleCount = CoverageSampleCount - }; - - Texture* createdStencilTexture = this.webGPU.DeviceCreateTexture(device, in stencilTextureDescriptor); - if (createdStencilTexture is null) - { - this.ReleaseTextureViewLocked(createdMultisampleCoverageView); - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - TextureViewDescriptor stencilViewDescriptor = new() - { - Format = TextureFormat.Depth24PlusStencil8, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - TextureView* createdStencilView = this.webGPU.TextureCreateView(createdStencilTexture, in stencilViewDescriptor); - if (createdStencilView is null) - { - this.ReleaseTextureLocked(createdStencilTexture); - this.ReleaseTextureViewLocked(createdMultisampleCoverageView); - this.ReleaseTextureLocked(createdMultisampleCoverageTexture); - return false; - } - - this.coverageScratchMultisampleTexture = createdMultisampleCoverageTexture; - this.coverageScratchMultisampleView = createdMultisampleCoverageView; - this.coverageScratchStencilTexture = createdStencilTexture; - this.coverageScratchStencilView = createdStencilView; - this.coverageScratchWidth = width; - this.coverageScratchHeight = height; - - multisampleCoverageView = createdMultisampleCoverageView; - stencilView = createdStencilView; - return true; - } - - private bool TryEnsureCoverageScratchVertexBufferLocked(Device* device, ulong requiredByteCount) - { - if (this.coverageScratchVertexBuffer is not null && - this.coverageScratchVertexCapacityBytes >= requiredByteCount) - { - return true; - } - - this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); - this.coverageScratchVertexBuffer = null; - this.coverageScratchVertexCapacityBytes = 0; - - BufferDescriptor vertexBufferDescriptor = new() - { - Usage = BufferUsage.Vertex | BufferUsage.CopyDst, - Size = requiredByteCount - }; - - WgpuBuffer* createdVertexBuffer = this.webGPU.DeviceCreateBuffer(device, in vertexBufferDescriptor); - if (createdVertexBuffer is null) - { - return false; - } - - this.coverageScratchVertexBuffer = createdVertexBuffer; - this.coverageScratchVertexCapacityBytes = requiredByteCount; - return true; - } - - /// - /// Rasterizes edge triangles through a stencil-and-cover pass into an R8Unorm texture. - /// - private bool TryRasterizeCoverageTextureLocked( - in CoverageTriangleData coverageTriangleData, - in RasterizerOptions rasterizerOptions, - Device* device, - Queue* queue, - out Texture* coverageTexture, - out TextureView* coverageView) - { - coverageTexture = null; - coverageView = null; - - Texture* createdCoverageTexture = null; - TextureView* createdCoverageView = null; - CommandEncoder* commandEncoder = null; - RenderPassEncoder* passEncoder = null; - CommandBuffer* commandBuffer = null; - bool success = false; - try - { - if (!this.TryEnsureCoverageScratchTargetsLocked( - device, - rasterizerOptions.Interest.Width, - rasterizerOptions.Interest.Height, - out TextureView* multisampleCoverageView, - out TextureView* stencilView)) - { - return false; - } - - TextureDescriptor coverageTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)rasterizerOptions.Interest.Width, (uint)rasterizerOptions.Interest.Height, 1), - Format = TextureFormat.R8Unorm, - MipLevelCount = 1, - SampleCount = 1 - }; - - createdCoverageTexture = this.webGPU.DeviceCreateTexture(device, in coverageTextureDescriptor); - if (createdCoverageTexture is null) - { - return false; - } - - TextureViewDescriptor coverageViewDescriptor = new() - { - Format = TextureFormat.R8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - createdCoverageView = this.webGPU.TextureCreateView(createdCoverageTexture, in coverageViewDescriptor); - if (createdCoverageView is null) - { - return false; - } - - ulong vertexByteCount = checked(coverageTriangleData.TotalVertexCount * (ulong)Unsafe.SizeOf()); - if (!this.TryEnsureCoverageScratchVertexBufferLocked(device, vertexByteCount) || this.coverageScratchVertexBuffer is null) - { - return false; - } - - fixed (StencilVertex* verticesPtr = coverageTriangleData.Vertices) - { - this.webGPU.QueueWriteBuffer(queue, this.coverageScratchVertexBuffer, 0, verticesPtr, (nuint)vertexByteCount); - } - - CommandEncoderDescriptor commandEncoderDescriptor = default; - commandEncoder = this.webGPU.DeviceCreateCommandEncoder(device, in commandEncoderDescriptor); - if (commandEncoder is null) - { - return false; - } - - RenderPassColorAttachment colorAttachment = new() - { - View = multisampleCoverageView, - ResolveTarget = createdCoverageView, - LoadOp = LoadOp.Clear, - StoreOp = StoreOp.Discard, - ClearValue = default - }; - - RenderPassDepthStencilAttachment depthStencilAttachment = new() - { - View = stencilView, - DepthLoadOp = LoadOp.Clear, - DepthStoreOp = StoreOp.Discard, - DepthClearValue = 1F, - DepthReadOnly = false, - StencilLoadOp = LoadOp.Clear, - StencilStoreOp = StoreOp.Discard, - StencilClearValue = 0, - StencilReadOnly = false - }; - - RenderPassDescriptor renderPassDescriptor = new() - { - ColorAttachmentCount = 1, - ColorAttachments = &colorAttachment, - DepthStencilAttachment = &depthStencilAttachment - }; - - passEncoder = this.webGPU.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); - if (passEncoder is null) - { - return false; - } - - this.webGPU.RenderPassEncoderSetStencilReference(passEncoder, 0); - this.webGPU.RenderPassEncoderSetVertexBuffer(passEncoder, 0, this.coverageScratchVertexBuffer, 0, vertexByteCount); - if (rasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd) - { - this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilEvenOddPipeline); - this.webGPU.RenderPassEncoderDraw(passEncoder, coverageTriangleData.TotalVertexCount, 1, 0, 0); - } - else - { - if (coverageTriangleData.IncrementVertexCount > 0) - { - this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroIncrementPipeline); - this.webGPU.RenderPassEncoderDraw(passEncoder, coverageTriangleData.IncrementVertexCount, 1, 0, 0); - } - - if (coverageTriangleData.DecrementVertexCount > 0) - { - this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageStencilNonZeroDecrementPipeline); - this.webGPU.RenderPassEncoderDraw( - passEncoder, - coverageTriangleData.DecrementVertexCount, - 1, - coverageTriangleData.IncrementVertexCount, - 0); - } - } - - this.webGPU.RenderPassEncoderSetStencilReference(passEncoder, 0); - this.webGPU.RenderPassEncoderSetPipeline(passEncoder, this.coverageCoverPipeline); - this.webGPU.RenderPassEncoderDraw(passEncoder, CoverageCoverVertexCount, 1, 0, 0); - - this.webGPU.RenderPassEncoderEnd(passEncoder); - this.webGPU.RenderPassEncoderRelease(passEncoder); - passEncoder = null; - - CommandBufferDescriptor commandBufferDescriptor = default; - commandBuffer = this.webGPU.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); - if (commandBuffer is null) - { - return false; - } - - this.webGPU.QueueSubmit(queue, 1, ref commandBuffer); - - this.webGPU.CommandBufferRelease(commandBuffer); - commandBuffer = null; - coverageTexture = createdCoverageTexture; - coverageView = createdCoverageView; - createdCoverageTexture = null; - createdCoverageView = null; - success = true; - return true; - } - finally - { - if (passEncoder is not null) - { - this.webGPU.RenderPassEncoderRelease(passEncoder); - } - - if (commandBuffer is not null) - { - this.webGPU.CommandBufferRelease(commandBuffer); - } - - if (commandEncoder is not null) - { - this.webGPU.CommandEncoderRelease(commandEncoder); - } - - if (!success) - { - this.ReleaseTextureViewLocked(createdCoverageView); - this.ReleaseTextureLocked(createdCoverageTexture); - } - } - } - - /// - /// Flattens a path into local-interest coordinates and converts each non-horizontal edge - /// into a trapezoid (two triangles) anchored at a left-side sentinel X. - /// - private static bool TryBuildCoverageTriangles( - IPath path, - Point interestLocation, - Size interestSize, - RasterizerSamplingOrigin samplingOrigin, - out CoverageTriangleData coverageTriangleData) - { - coverageTriangleData = default; - if (interestSize.Width <= 0 || interestSize.Height <= 0) - { - return false; - } - - float sampleShift = samplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - float offsetX = sampleShift - interestLocation.X; - float offsetY = sampleShift - interestLocation.Y; - - List segments = []; - float minX = float.PositiveInfinity; - - foreach (ISimplePath simplePath in path.Flatten()) - { - ReadOnlySpan points = simplePath.Points.Span; - if (points.Length < 2) - { - continue; - } - - for (int i = 1; i < points.Length; i++) - { - AddCoverageSegment(points[i - 1], points[i], offsetX, offsetY, segments, ref minX); - } - - if (simplePath.IsClosed) - { - AddCoverageSegment(points[^1], points[0], offsetX, offsetY, segments, ref minX); - } - } - - if (segments.Count == 0 || !float.IsFinite(minX)) - { - return false; - } - - int incrementEdgeCount = 0; - int decrementEdgeCount = 0; - foreach (CoverageSegment segment in segments) - { - if (segment.FromY == segment.ToY) - { - continue; - } - - if (segment.ToY > segment.FromY) - { - incrementEdgeCount++; - } - else - { - decrementEdgeCount++; - } - } - - int totalEdgeCount = incrementEdgeCount + decrementEdgeCount; - if (totalEdgeCount == 0) - { - return false; - } - - float sentinelX = minX - 1F; - float widthScale = 2F / interestSize.Width; - float heightScale = 2F / interestSize.Height; - int incrementVertexCount = checked(incrementEdgeCount * 6); - int decrementVertexCount = checked(decrementEdgeCount * 6); - StencilVertex[] vertices = new StencilVertex[checked(incrementVertexCount + decrementVertexCount)]; - - int vertexIndex = 0; - foreach (CoverageSegment segment in segments) - { - if (segment.ToY <= segment.FromY) - { - continue; - } - - AppendCoverageEdgeQuad( - vertices, - ref vertexIndex, - sentinelX, - segment.FromX, - segment.FromY, - segment.ToX, - segment.ToY, - widthScale, - heightScale); - } - - int decrementStartIndex = incrementVertexCount; - vertexIndex = decrementStartIndex; - foreach (CoverageSegment segment in segments) - { - if (segment.ToY >= segment.FromY) - { - continue; - } - - AppendCoverageEdgeQuad( - vertices, - ref vertexIndex, - sentinelX, - segment.FromX, - segment.FromY, - segment.ToX, - segment.ToY, - widthScale, - heightScale); - } - - coverageTriangleData = new CoverageTriangleData( - vertices, - (uint)incrementVertexCount, - (uint)decrementVertexCount); - return true; - } - - private static void AddCoverageSegment( - PointF from, - PointF to, - float offsetX, - float offsetY, - List destination, - ref float minX) - { - if (from.Equals(to)) - { - return; - } - - if (!float.IsFinite(from.X) || - !float.IsFinite(from.Y) || - !float.IsFinite(to.X) || - !float.IsFinite(to.Y)) - { - return; - } - - float fromX = from.X + offsetX; - float fromY = from.Y + offsetY; - float toX = to.X + offsetX; - float toY = to.Y + offsetY; - - destination.Add(new CoverageSegment(fromX, fromY, toX, toY)); - minX = MathF.Min(minX, MathF.Min(fromX, toX)); - } - - private static void AppendCoverageEdgeQuad( - StencilVertex[] destination, - ref int destinationIndex, - float sentinelX, - float fromX, - float fromY, - float toX, - float toY, - float widthScale, - float heightScale) - { - StencilVertex a = ToStencilVertex(sentinelX, fromY, widthScale, heightScale); - StencilVertex b = ToStencilVertex(fromX, fromY, widthScale, heightScale); - StencilVertex c = ToStencilVertex(toX, toY, widthScale, heightScale); - StencilVertex d = ToStencilVertex(sentinelX, toY, widthScale, heightScale); - - destination[destinationIndex++] = a; - destination[destinationIndex++] = b; - destination[destinationIndex++] = c; - destination[destinationIndex++] = a; - destination[destinationIndex++] = c; - destination[destinationIndex++] = d; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static StencilVertex ToStencilVertex(float x, float y, float widthScale, float heightScale) - => new() - { - X = (x * widthScale) - 1F, - Y = 1F - (y * heightScale) - }; - - private void ReleaseCoverageScratchResourcesLocked() - { - this.ReleaseBufferLocked(this.coverageScratchVertexBuffer); - this.ReleaseTextureViewLocked(this.coverageScratchStencilView); - this.ReleaseTextureLocked(this.coverageScratchStencilTexture); - this.ReleaseTextureViewLocked(this.coverageScratchMultisampleView); - this.ReleaseTextureLocked(this.coverageScratchMultisampleTexture); - this.coverageScratchVertexBuffer = null; - this.coverageScratchVertexCapacityBytes = 0; - this.coverageScratchStencilView = null; - this.coverageScratchStencilTexture = null; - this.coverageScratchMultisampleView = null; - this.coverageScratchMultisampleTexture = null; - this.coverageScratchWidth = 0; - this.coverageScratchHeight = 0; - } - - private void ReleaseTextureViewLocked(TextureView* textureView) - { - if (textureView is null) - { - return; - } - - this.webGPU.TextureViewRelease(textureView); - } - - private void ReleaseTextureLocked(Texture* texture) - { - if (texture is null) - { - return; - } - - this.webGPU.TextureRelease(texture); - } - - private void ReleaseBufferLocked(WgpuBuffer* buffer) - { - if (buffer is null) - { - return; - } - - this.webGPU.BufferRelease(buffer); - } - - private struct StencilVertex - { - public float X; - public float Y; - } - - private readonly struct CoverageSegment - { - public CoverageSegment(float fromX, float fromY, float toX, float toY) - { - this.FromX = fromX; - this.FromY = fromY; - this.ToX = toX; - this.ToY = toY; - } - - public float FromX { get; } - - public float FromY { get; } - - public float ToX { get; } - - public float ToY { get; } - } - - private readonly struct CoverageTriangleData - { - public CoverageTriangleData(StencilVertex[] vertices, uint incrementVertexCount, uint decrementVertexCount) - { - this.Vertices = vertices; - this.IncrementVertexCount = incrementVertexCount; - this.DecrementVertexCount = decrementVertexCount; - } - - public StencilVertex[] Vertices { get; } - - public uint IncrementVertexCount { get; } - - public uint DecrementVertexCount { get; } - - public uint TotalVertexCount => this.IncrementVertexCount + this.DecrementVertexCount; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs index b0589520d..bf7671709 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -7,6 +7,7 @@ using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -114,6 +115,53 @@ internal static bool TryCreate( return true; } + internal static bool TryWriteTexture( + WebGPUDrawingBackend backend, + nint textureHandle, + int width, + int height, + Image image, + out string error) + where TPixel : unmanaged, IPixel + { + if (textureHandle == 0) + { + error = "Texture handle is zero."; + return false; + } + + if (image.Width != width || image.Height != height) + { + error = "Source image dimensions must match the target texture dimensions."; + return false; + } + + if (!backend.TryGetInteropHandles(out _, out nint queueHandle)) + { + error = backend.TestingLastGPUInitializationFailure ?? "WebGPU backend is not initialized."; + return false; + } + + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + Buffer2DRegion sourceRegion = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + WebGPUFlushContext.UploadTextureFromRegion( + lease.Api, + (Queue*)queueHandle, + (Texture*)textureHandle, + sourceRegion, + Configuration.Default.MemoryAllocator); + error = string.Empty; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + internal static bool TryReadTexture( WebGPUDrawingBackend backend, nint textureHandle, diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index 89e2085e5..67d4520d8 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -161,6 +161,7 @@ private static int ComputeCoverageDefinitionKeySlow(IPath path, in RasterizerOpt foreach (ISimplePath simplePath in path.Flatten()) { ReadOnlySpan points = simplePath.Points.Span; + hash.Add(simplePath.IsClosed); hash.Add(points.Length); for (int i = 0; i < points.Length; i++) { diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index b1e181bef..d0a134742 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -114,7 +114,7 @@ public void Cleanup() public void DrawingCanvasDefaultBackend() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.defaultImage)); - this.ClearWithDrawingCanvas(this.defaultConfiguration, frame); + // this.ClearWithDrawingCanvas(this.defaultConfiguration, frame); using DrawingCanvas canvas = new(this.defaultConfiguration, frame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); @@ -124,7 +124,7 @@ public void DrawingCanvasDefaultBackend() public void DrawingCanvasWebGPUBackendCpuRegion() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); - this.ClearWithDrawingCanvas(this.webGpuConfiguration, frame); + // this.ClearWithDrawingCanvas(this.webGpuConfiguration, frame); using DrawingCanvas canvas = new(this.webGpuConfiguration, frame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); @@ -133,7 +133,7 @@ public void DrawingCanvasWebGPUBackendCpuRegion() [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] public void DrawingCanvasWebGPUBackendNativeSurface() { - this.ClearWithDrawingCanvas(this.webGpuConfiguration, this.webGpuNativeFrame); + // this.ClearWithDrawingCanvas(this.webGpuConfiguration, this.webGpuNativeFrame); using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame); canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); canvas.Flush(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 9c2fe2350..db81d0437 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -79,7 +79,7 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } [Theory] @@ -94,7 +94,6 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid GraphicsOptions clearOptions = new() { - Antialias = false, AlphaCompositionMode = PixelAlphaCompositionMode.Src, ColorBlendingMode = PixelColorBlendingMode.Normal, BlendPercentage = 1F @@ -392,7 +391,11 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 4F); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.2F); + Rectangle textRegion = Rectangle.Intersect( + new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), + new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); + AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.03F); } [Theory] @@ -498,7 +501,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef } [Theory] - [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] + [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -555,7 +558,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi } [Theory] - [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] + [WithBlankImage(1200, 280, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -745,22 +748,15 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( new(configuration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface)); if (initialImage is not null) { - DrawingOptions copyOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - BlendPercentage = 1F, - ColorBlendingMode = PixelColorBlendingMode.Normal, - AlphaCompositionMode = PixelAlphaCompositionMode.Src - } - }; - - canvas.DrawImage( - initialImage, - initialImage.Bounds, - new RectangleF(0, 0, width, height), - copyOptions); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryWriteTexture( + backend, + textureHandle, + width, + height, + initialImage, + out string uploadError), + uploadError); } drawAction(canvas); @@ -816,9 +812,24 @@ private static void AssertBackendTripletSimilarity( float defaultTolerancePercent) where TPixel : unmanaged, IPixel { - ImageComparer.Exact.VerifySimilarity(cpuRegionImage, nativeSurfaceImage); + ImageComparer.TolerantPercentage(0.01F).VerifySimilarity(cpuRegionImage, nativeSurfaceImage); ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(defaultTolerancePercent); tolerantComparer.VerifySimilarity(defaultImage, cpuRegionImage); + tolerantComparer.VerifySimilarity(defaultImage, nativeSurfaceImage); + } + + private static void AssertBackendTripletSimilarityInRegion( + Image defaultImage, + Image cpuRegionImage, + Image nativeSurfaceImage, + Rectangle region, + float defaultTolerancePercent) + where TPixel : unmanaged, IPixel + { + using Image defaultRegion = defaultImage.Clone(ctx => ctx.Crop(region)); + using Image cpuRegion = cpuRegionImage.Clone(ctx => ctx.Crop(region)); + using Image nativeRegion = nativeSurfaceImage.Clone(ctx => ctx.Crop(region)); + AssertBackendTripletSimilarity(defaultRegion, cpuRegion, nativeRegion, defaultTolerancePercent); } private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backend) From 06a83266c73e83f52c99c5f9af1c04939424f7bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 27 Feb 2026 18:06:19 +1000 Subject: [PATCH 026/136] Composite: switch to tile-based dispatch --- .../Shaders/PreparedCompositeComputeShader.cs | 25 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 72 ++-- .../WebGPUDrawingBackend.cs | 364 ++++++++++++++---- .../Backends/WebGPUDrawingBackendTests.cs | 78 ++-- 4 files changed, 377 insertions(+), 162 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs index da21ec2bd..d6f494513 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs @@ -36,10 +36,15 @@ struct Params { solid_a: u32, }; - struct DispatchConfig { + struct TileRange { + command_start: u32, command_count: u32, + }; + + struct DispatchConfig { target_width: u32, target_height: u32, + tile_count_x: u32, pad0: u32, }; @@ -47,7 +52,9 @@ struct DispatchConfig { @group(0) @binding(1) var source_texture: texture_2d; @group(0) @binding(2) var destination_pixels: array>; @group(0) @binding(3) var commands: array; - @group(0) @binding(4) var dispatch_config: DispatchConfig; + @group(0) @binding(4) var tile_ranges: array; + @group(0) @binding(5) var tile_command_indices: array; + @group(0) @binding(6) var dispatch_config: DispatchConfig; fn u32_to_f32(bits: u32) -> f32 { return bitcast(bits); @@ -179,17 +186,25 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { return; } + let tile_width: u32 = 16u; + let tile_height: u32 = 16u; + let tile_x = global_id.x / tile_width; + let tile_y = global_id.y / tile_height; + let tile_index = (tile_y * dispatch_config.tile_count_x) + tile_x; + let tile_range = tile_ranges[tile_index]; + let dest_x = i32(global_id.x); let dest_y = i32(global_id.y); let dest_index = (global_id.y * dispatch_config.target_width) + global_id.x; var destination = destination_pixels[dest_index]; - var command_index: u32 = 0u; + var tile_command_offset: u32 = 0u; loop { - if (command_index >= dispatch_config.command_count) { + if (tile_command_offset >= tile_range.command_count) { break; } + let command_index = tile_command_indices[tile_range.command_start + tile_command_offset]; let command = commands[command_index]; let command_min_x = i32(command.destination_x); let command_min_y = i32(command.destination_y); @@ -228,7 +243,7 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { } } - command_index = command_index + 1u; + tile_command_offset = tile_command_offset + 1u; } destination_pixels[dest_index] = destination; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index ccd2ae774..336c36ded 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -138,6 +138,7 @@ private bool TryCreateCoverageTextureFromFlattened( atlasWidth, atlasHeight, configuration.MemoryAllocator, + totalLineCount == 0, out Texture* coverageTexture, out coverageView, out error)) @@ -201,7 +202,6 @@ private bool TryCreateCoverageTextureFromFlattened( int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; - pathUpload.Clear(); int tileBase = 0; for (int i = 0; i < pathBuilds.Length; i++) { @@ -249,20 +249,11 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - using (IMemoryOwner tileZeroOwner = configuration.MemoryAllocator.Allocate(tileBufferBytes)) - { - Span tileZero = tileZeroOwner.Memory.Span[..tileBufferBytes]; - tileZero.Clear(); - fixed (byte* tilePtr = tileZero) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileBuffer, - 0, - tilePtr, - (nuint)tileBufferBytes); - } - } + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileBuffer, + 0, + (nuint)tileBufferBytes); int tileCountsBytes = checked(totalTileCount * sizeof(uint)); if (!TryGetOrCreateCoverageBuffer( @@ -276,20 +267,11 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - using (IMemoryOwner tileCountsZeroOwner = configuration.MemoryAllocator.Allocate(tileCountsBytes)) - { - Span tileCountsZero = tileCountsZeroOwner.Memory.Span[..tileCountsBytes]; - tileCountsZero.Clear(); - fixed (byte* tileCountsPtr = tileCountsZero) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileCountsBuffer, - 0, - tileCountsPtr, - (nuint)tileCountsBytes); - } - } + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileCountsBuffer, + 0, + (nuint)tileCountsBytes); if (totalEstimatedSegments > int.MaxValue) { @@ -311,20 +293,11 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - using (IMemoryOwner segCountsZeroOwner = configuration.MemoryAllocator.Allocate(segCountsBytes)) - { - Span segCountsZero = segCountsZeroOwner.Memory.Span[..segCountsBytes]; - segCountsZero.Clear(); - fixed (byte* segCountsPtr = segCountsZero) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - segCountsBuffer, - 0, - segCountsPtr, - (nuint)segCountsBytes); - } - } + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + segCountsBuffer, + 0, + (nuint)segCountsBytes); int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); if (!TryGetOrCreateCoverageBuffer( @@ -521,6 +494,7 @@ private bool TryCreateCoverageTextureFromFlattened( interest.Width, interest.Height, configuration.MemoryAllocator, + lineCount == 0, out Texture* coverageTexture, out coverageView, out error)) @@ -587,7 +561,6 @@ private bool TryCreateCoverageTextureFromFlattened( } Span pathBytes = stackalloc byte[PathStrideBytes]; - pathBytes.Clear(); WritePath(pathBytes, (uint)tileMinX, (uint)tileMinY, (uint)tileMaxX, (uint)tileMaxY); BufferDescriptor pathDescriptor = new() @@ -1118,6 +1091,9 @@ private static void WritePath(Span destination, uint x0, uint y0, uint x1, BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8, 4), x1); BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), y1); BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16, 4), tiles); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(20, 4), 0u); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(24, 4), 0u); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(28, 4), 0u); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1171,6 +1147,7 @@ private static bool TryCreateCoverageTexture( int width, int height, MemoryAllocator allocator, + bool clearOnCreate, out Texture* coverageTexture, out TextureView* coverageView, out string? error) @@ -1212,10 +1189,11 @@ private static bool TryCreateCoverageTexture( return false; } - int rowBytes = checked(width * sizeof(float)); - int byteCount = checked(rowBytes * height); - using (IMemoryOwner zeroOwner = allocator.Allocate(byteCount)) + if (clearOnCreate) { + int rowBytes = checked(width * sizeof(float)); + int byteCount = checked(rowBytes * height); + using IMemoryOwner zeroOwner = allocator.Allocate(byteCount); Span zeroData = zeroOwner.Memory.Span[..byteCount]; zeroData.Clear(); ImageCopyTexture destination = new() diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 2c0e36536..d4b4b46f2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -43,9 +42,15 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi { private const uint CompositeVertexCount = 6; private const int CompositeComputeWorkgroupSize = 8; + private const int CompositeTileWidth = 16; + private const int CompositeTileHeight = 16; private const int CompositeDestinationPixelStride = 16; private const uint PreparedBrushTypeSolid = 0; private const uint PreparedBrushTypeImage = 1; + private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; + private const string PreparedCompositeTileRangesBufferKey = "prepared-composite/tile-ranges"; + private const string PreparedCompositeTileIndicesBufferKey = "prepared-composite/tile-indices"; + private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; @@ -207,6 +212,7 @@ public void FlushCompositions( return; } + Rectangle targetExtent = new(0, 0, target.Bounds.Width, target.Bounds.Height); int commandCount = 0; Rectangle? compositionBounds = null; for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) @@ -215,13 +221,18 @@ public void FlushCompositions( IReadOnlyList commands = batch.Commands; for (int i = 0; i < commands.Count; i++) { - Rectangle destination = commands[i].DestinationRegion; + Rectangle destination = Rectangle.Intersect(commands[i].DestinationRegion, targetExtent); + if (destination.Width <= 0 || destination.Height <= 0) + { + continue; + } + compositionBounds = compositionBounds.HasValue ? Rectangle.Union(compositionBounds.Value, destination) : destination; - } - commandCount += commands.Count; + commandCount++; + } } if (commandCount == 0) @@ -488,21 +499,44 @@ destinationPixelsBuffer is not null && continue; } - int definitionKey = batch.Definition.DefinitionKey; - if (!coverageDefinitionIndexByKey.TryGetValue(definitionKey, out int coverageDefinitionIndex)) + IReadOnlyList commands = batch.Commands; + bool sawVisibleCommand = false; + int coverageDefinitionIndex = -1; + for (int commandIndex = 0; commandIndex < commands.Count; commandIndex++) { - coverageDefinitionIndex = coverageDefinitions.Count; - coverageDefinitions.Add(batch.Definition); - coverageDefinitionIndexByKey.Add(definitionKey, coverageDefinitionIndex); + PreparedCompositionCommand command = commands[commandIndex]; + Rectangle clippedDestination = Rectangle.Intersect(command.DestinationRegion, targetLocalBounds); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + continue; + } + + if (!sawVisibleCommand) + { + int definitionKey = batch.Definition.DefinitionKey; + if (!coverageDefinitionIndexByKey.TryGetValue(definitionKey, out coverageDefinitionIndex)) + { + coverageDefinitionIndex = coverageDefinitions.Count; + coverageDefinitions.Add(batch.Definition); + coverageDefinitionIndexByKey.Add(definitionKey, coverageDefinitionIndex); + } + + sawVisibleCommand = true; + } + + pendingCommands.Add(new PreparedCompositePendingCommand(coverageDefinitionIndex, command)); } - IReadOnlyList commands = batch.Commands; - for (int commandIndex = 0; commandIndex < commands.Count; commandIndex++) + if (sawVisibleCommand) { - pendingCommands.Add(new PreparedCompositePendingCommand(coverageDefinitionIndex, commands[commandIndex])); + this.TestingComputePathBatchCount++; } + } - this.TestingComputePathBatchCount++; + if (pendingCommands.Count == 0) + { + error = null; + return true; } if (!this.TryCreateCoverageTextureFromFlattened( @@ -579,13 +613,23 @@ private bool TryDispatchPreparedCompositeCommands( return false; } + int tileCountX = checked((int)DivideRoundUp(targetLocalBounds.Width, CompositeTileWidth)); + int tileCountY = checked((int)DivideRoundUp(targetLocalBounds.Height, CompositeTileHeight)); + int tileCount = checked(tileCountX * tileCountY); + if (tileCount == 0) + { + return true; + } + uint parameterSize = (uint)Unsafe.SizeOf(); - int parameterUploadByteCount = checked((int)(parameterSize * (uint)compositeCommands.Count)); - IMemoryOwner parametersUploadOwner = flushContext.MemoryAllocator.Allocate(parameterUploadByteCount); + IMemoryOwner parametersOwner = + flushContext.MemoryAllocator.Allocate(compositeCommands.Count); + IMemoryOwner validTileCommandsOwner = + flushContext.MemoryAllocator.Allocate(compositeCommands.Count); try { - Span parameterUpload = parametersUploadOwner.Memory.Span[..parameterUploadByteCount]; - parameterUpload.Clear(); + Span parameters = parametersOwner.Memory.Span[..compositeCommands.Count]; + Span validTileCommands = validTileCommandsOwner.Memory.Span[..compositeCommands.Count]; TextureView* sourceTextureView = defaultBrushTextureView; nint sourceTextureViewHandle = (nint)defaultBrushTextureView; bool hasImageTexture = false; @@ -670,7 +714,18 @@ private bool TryDispatchPreparedCompositeCommands( int destinationX = command.DestinationRegion.X - targetLocalBounds.X; int destinationY = command.DestinationRegion.Y - targetLocalBounds.Y; - PreparedCompositeParameters parameters = new( + int destinationMaxX = destinationX + command.DestinationRegion.Width - 1; + int destinationMaxY = destinationY + command.DestinationRegion.Height - 1; + int minTileX = Math.Max(0, destinationX / CompositeTileWidth); + int minTileY = Math.Max(0, destinationY / CompositeTileHeight); + int maxTileX = Math.Min(tileCountX - 1, destinationMaxX / CompositeTileWidth); + int maxTileY = Math.Min(tileCountY - 1, destinationMaxY / CompositeTileHeight); + if (maxTileX < minTileX || maxTileY < minTileY) + { + continue; + } + + PreparedCompositeParameters commandParameters = new( destinationX, destinationY, command.DestinationRegion.Width, @@ -690,10 +745,15 @@ private bool TryDispatchPreparedCompositeCommands( command.GraphicsOptions.BlendPercentage, solidColor); - int parameterOffset = checked((int)(validCommandCount * parameterSize)); - MemoryMarshal.Write( - parameterUpload.Slice(parameterOffset, (int)parameterSize), - in parameters); + uint parameterIndex = validCommandCount; + parameters[(int)parameterIndex] = commandParameters; + + validTileCommands[(int)parameterIndex] = new PreparedCompositeTileCommand( + parameterIndex, + minTileX, + minTileY, + maxTileX, + maxTileY); validCommandCount++; } @@ -704,63 +764,163 @@ private bool TryDispatchPreparedCompositeCommands( return true; } - int usedParameterByteCount = checked((int)(validCommandCount * parameterSize)); - BufferDescriptor paramsDescriptor = new() + if (!hasCoverageTexture) { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)usedParameterByteCount - }; + error = "Prepared composite flush did not produce a coverage texture."; + return false; + } - WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); - if (paramsBuffer is null) + using IMemoryOwner tileCommandCountsOwner = flushContext.MemoryAllocator.Allocate(tileCount); + using IMemoryOwner tileCommandStartsOwner = flushContext.MemoryAllocator.Allocate(tileCount); + using IMemoryOwner tileCommandWriteOffsetsOwner = flushContext.MemoryAllocator.Allocate(tileCount); + Span tileCommandCounts = tileCommandCountsOwner.Memory.Span[..tileCount]; + Span tileCommandStarts = tileCommandStartsOwner.Memory.Span[..tileCount]; + Span tileCommandWriteOffsets = tileCommandWriteOffsetsOwner.Memory.Span[..tileCount]; + tileCommandCounts.Clear(); + + for (int commandIndex = 0; commandIndex < validCommandCount; commandIndex++) + { + PreparedCompositeTileCommand command = validTileCommands[commandIndex]; + for (int tileY = command.MinTileY; tileY <= command.MaxTileY; tileY++) + { + int rowOffset = checked(tileY * tileCountX); + for (int tileX = command.MinTileX; tileX <= command.MaxTileX; tileX++) + { + tileCommandCounts[rowOffset + tileX]++; + } + } + } + + int totalTileCommandIndices = 0; + for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + tileCommandStarts[tileIndex] = totalTileCommandIndices; + totalTileCommandIndices = checked(totalTileCommandIndices + tileCommandCounts[tileIndex]); + } + + if (totalTileCommandIndices == 0) + { + error = null; + return true; + } + + tileCommandStarts.CopyTo(tileCommandWriteOffsets); + using IMemoryOwner tileCommandIndicesOwner = flushContext.MemoryAllocator.Allocate(totalTileCommandIndices); + Span tileCommandIndices = tileCommandIndicesOwner.Memory.Span[..totalTileCommandIndices]; + + for (int commandIndex = 0; commandIndex < validCommandCount; commandIndex++) + { + PreparedCompositeTileCommand command = validTileCommands[commandIndex]; + for (int tileY = command.MinTileY; tileY <= command.MaxTileY; tileY++) + { + int rowOffset = checked(tileY * tileCountX); + for (int tileX = command.MinTileX; tileX <= command.MaxTileX; tileX++) + { + int tileIndex = rowOffset + tileX; + int writeOffset = tileCommandWriteOffsets[tileIndex]++; + tileCommandIndices[writeOffset] = command.ParameterIndex; + } + } + } + + using IMemoryOwner tileRangesOwner = flushContext.MemoryAllocator.Allocate(tileCount); + Span tileRanges = tileRangesOwner.Memory.Span[..tileCount]; + for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) + { + tileRanges[tileIndex] = new PreparedCompositeTileRange((uint)tileCommandStarts[tileIndex], (uint)tileCommandCounts[tileIndex]); + } + + int usedParameterByteCount = checked((int)(validCommandCount * parameterSize)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeParamsBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)usedParameterByteCount, + out WgpuBuffer* paramsBuffer, + out _, + out error)) { - error = "Failed to create composite parameter buffer."; return false; } - flushContext.TrackBuffer(paramsBuffer); - fixed (byte* parameterUploadPtr = parameterUpload) + Span usedParameters = parameters[..(int)validCommandCount]; + fixed (PreparedCompositeParameters* usedParametersPtr = usedParameters) { flushContext.Api.QueueWriteBuffer( flushContext.Queue, paramsBuffer, 0, - parameterUploadPtr, + usedParametersPtr, (nuint)usedParameterByteCount); } - BufferDescriptor dispatchConfigDescriptor = new() + int tileRangesByteCount = checked(tileCount * Unsafe.SizeOf()); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeTileRangesBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileRangesByteCount, + out WgpuBuffer* tileRangesBuffer, + out _, + out error)) { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; + return false; + } - WgpuBuffer* dispatchConfigBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in dispatchConfigDescriptor); - if (dispatchConfigBuffer is null) + fixed (PreparedCompositeTileRange* tileRangesPtr = tileRanges) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileRangesBuffer, + 0, + tileRangesPtr, + (nuint)tileRangesByteCount); + } + + int tileCommandIndicesByteCount = checked(totalTileCommandIndices * sizeof(uint)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeTileIndicesBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileCommandIndicesByteCount, + out WgpuBuffer* tileCommandIndicesBuffer, + out _, + out error)) + { + return false; + } + + fixed (uint* tileCommandIndicesPtr = tileCommandIndices) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + tileCommandIndicesBuffer, + 0, + tileCommandIndicesPtr, + (nuint)tileCommandIndicesByteCount); + } + + nuint dispatchConfigSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeDispatchConfigBufferKey, + BufferUsage.Uniform | BufferUsage.CopyDst, + dispatchConfigSize, + out WgpuBuffer* dispatchConfigBuffer, + out _, + out error)) { - error = "Failed to create composite dispatch config buffer."; return false; } - flushContext.TrackBuffer(dispatchConfigBuffer); PreparedCompositeDispatchConfig dispatchConfig = new( - validCommandCount, (uint)targetLocalBounds.Width, - (uint)targetLocalBounds.Height); + (uint)targetLocalBounds.Height, + (uint)tileCountX); flushContext.Api.QueueWriteBuffer( flushContext.Queue, dispatchConfigBuffer, 0, &dispatchConfig, - (nuint)Unsafe.SizeOf()); + dispatchConfigSize); - if (!hasCoverageTexture) - { - error = "Prepared composite flush did not produce a coverage texture."; - return false; - } - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[5]; + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[7]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -788,15 +948,29 @@ private bool TryDispatchPreparedCompositeCommands( bindGroupEntries[4] = new BindGroupEntry { Binding = 4, + Buffer = tileRangesBuffer, + Offset = 0, + Size = (nuint)tileRangesByteCount + }; + bindGroupEntries[5] = new BindGroupEntry + { + Binding = 5, + Buffer = tileCommandIndicesBuffer, + Offset = 0, + Size = (nuint)tileCommandIndicesByteCount + }; + bindGroupEntries[6] = new BindGroupEntry + { + Binding = 6, Buffer = dispatchConfigBuffer, Offset = 0, - Size = (nuint)Unsafe.SizeOf() + Size = dispatchConfigSize }; BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 5, + EntryCount = 7, Entries = bindGroupEntries }; @@ -834,7 +1008,8 @@ private bool TryDispatchPreparedCompositeCommands( } finally { - parametersUploadOwner.Dispose(); + parametersOwner.Dispose(); + validTileCommandsOwner.Dispose(); } error = null; @@ -1277,7 +1452,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1327,6 +1502,28 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Binding = 4, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[5] = new BindGroupLayoutEntry + { + Binding = 5, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[6] = new BindGroupLayoutEntry + { + Binding = 6, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1336,7 +1533,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, + EntryCount = 7, Entries = entries }; @@ -1729,22 +1926,12 @@ private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memo private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEventSlim signal) { Wgpu? extension = flushContext.RuntimeLease.WgpuExtension; - if (extension is null) - { - return signal.Wait(CallbackTimeoutMilliseconds); - } - - long start = Stopwatch.GetTimestamp(); - while (!signal.IsSet && Stopwatch.GetElapsedTime(start).TotalMilliseconds < CallbackTimeoutMilliseconds) + if (extension is not null) { _ = extension.DevicePoll(flushContext.Device, true, (WrappedSubmissionIndex*)null); - if (!signal.IsSet) - { - _ = Thread.Yield(); - } } - return signal.IsSet; + return signal.Wait(CallbackTimeoutMilliseconds); } /// @@ -1847,18 +2034,53 @@ public CoveragePlacement(int originX, int originY, int width, int height) } [StructLayout(LayoutKind.Sequential)] - private readonly struct PreparedCompositeDispatchConfig + private readonly struct PreparedCompositeTileRange { + public readonly uint CommandStart; public readonly uint CommandCount; + + public PreparedCompositeTileRange(uint commandStart, uint commandCount) + { + this.CommandStart = commandStart; + this.CommandCount = commandCount; + } + } + + private readonly struct PreparedCompositeTileCommand + { + public PreparedCompositeTileCommand(uint parameterIndex, int minTileX, int minTileY, int maxTileX, int maxTileY) + { + this.ParameterIndex = parameterIndex; + this.MinTileX = minTileX; + this.MinTileY = minTileY; + this.MaxTileX = maxTileX; + this.MaxTileY = maxTileY; + } + + public uint ParameterIndex { get; } + + public int MinTileX { get; } + + public int MinTileY { get; } + + public int MaxTileX { get; } + + public int MaxTileY { get; } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeDispatchConfig + { public readonly uint TargetWidth; public readonly uint TargetHeight; + public readonly uint TileCountX; public readonly uint Pad0; - public PreparedCompositeDispatchConfig(uint commandCount, uint targetWidth, uint targetHeight) + public PreparedCompositeDispatchConfig(uint targetWidth, uint targetHeight, uint tileCountX) { - this.CommandCount = commandCount; this.TargetWidth = targetWidth; this.TargetHeight = targetHeight; + this.TileCountX = tileCountX; this.Pad0 = 0; } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index db81d0437..62d60e06f 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -42,14 +42,14 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); - Action> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.FillPath(polygon, brush, drawingOptions); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -57,7 +57,7 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "FillPath", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -104,25 +104,25 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid using Image foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); - Action> drawAction = canvas => + void DrawAction(DrawingCanvas canvas) { canvas.Fill(clearBrush, clearOptions); canvas.FillPath(polygon, brush, drawingOptions); - }; + } using Image defaultImage = new(384, 256); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = new(384, 256); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction); + (Action>)DrawAction); DebugSaveBackendTriplet(provider, "FillPath_ImageBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -188,14 +188,14 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); - Action> drawAction = canvas => canvas.FillPath(path, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.FillPath(path, brush, drawingOptions); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -203,7 +203,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "FillPath_NonZeroNestedContours", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -250,22 +250,22 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.FillPath(polygon, brush, drawingOptions); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = baseImage.Clone(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, baseImage); DebugSaveBackendTriplet( @@ -305,22 +305,22 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); - Action> drawAction = canvas => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.FillPath(polygon, brush, drawingOptions); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = baseImage.Clone(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, baseImage); DebugSaveBackendTriplet( @@ -356,15 +356,15 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag string text = "Sphinx of black quartz, judge my vow\n0123456789"; Brush brush = Brushes.Solid(Color.Black); Pen pen = Pens.Solid(Color.OrangeRed, 2F); - Action> drawAction = canvas => + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, drawingOptions, brush, pen); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -372,7 +372,7 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "DrawText", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -418,18 +418,18 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - Action> drawAction = canvas => + void DrawAction(DrawingCanvas canvas) { canvas.Fill(clearBrush, clearOptions); canvas.FillPath(polygon, brush, drawingOptions); - }; + } using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -437,7 +437,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceParity", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -469,19 +469,19 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef RectangularPolygon localPolygon = new(16.25F, 24.5F, 250.5F, 160.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); - Action> drawAction = canvas => + void DrawAction(DrawingCanvas canvas) { canvas.Fill(clearBrush, clearOptions); using DrawingCanvas regionCanvas = canvas.CreateRegion(region); regionCanvas.FillPath(localPolygon, brush, drawingOptions); - }; + } using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -489,7 +489,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -519,15 +519,15 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi string text = new('A', 200); Brush brush = Brushes.Solid(Color.Black); - Action> drawAction = canvas => + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, drawingOptions, brush, pen: null); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, drawAction); + RenderWithDefaultBackend(defaultImage, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -535,7 +535,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, - drawAction, + DrawAction, nativeSurfaceInitialImage); DebugSaveBackendTriplet(provider, "RepeatedGlyphs", defaultImage, cpuRegionImage, nativeSurfaceImage); From bd791794630ef73bdde873b95a99943f5eecb223 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 27 Feb 2026 22:30:17 +1000 Subject: [PATCH 027/136] Add GPU tile-count/prefix/scatter passes --- .../Shaders/PreparedCompositeComputeShader.cs | 35 +- ...PreparedCompositeTileCountComputeShader.cs | 106 +++ ...reparedCompositeTilePrefixComputeShader.cs | 50 ++ ...eparedCompositeTileScatterComputeShader.cs | 120 ++++ .../WebGPUDrawingBackend.cs | 624 ++++++++++++------ 5 files changed, 711 insertions(+), 224 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs index d6f494513..6c0de6708 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs @@ -36,25 +36,25 @@ struct Params { solid_a: u32, }; - struct TileRange { - command_start: u32, - command_count: u32, - }; - struct DispatchConfig { target_width: u32, target_height: u32, tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, pad0: u32, + pad1: u32, }; @group(0) @binding(0) var coverage_texture: texture_2d; @group(0) @binding(1) var source_texture: texture_2d; @group(0) @binding(2) var destination_pixels: array>; @group(0) @binding(3) var commands: array; - @group(0) @binding(4) var tile_ranges: array; - @group(0) @binding(5) var tile_command_indices: array; - @group(0) @binding(6) var dispatch_config: DispatchConfig; + @group(0) @binding(4) var tile_starts: array; + @group(0) @binding(5) var tile_counts: array>; + @group(0) @binding(6) var tile_command_indices: array; + @group(0) @binding(7) var dispatch_config: DispatchConfig; fn u32_to_f32(bits: u32) -> f32 { return bitcast(bits); @@ -191,7 +191,8 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { let tile_x = global_id.x / tile_width; let tile_y = global_id.y / tile_height; let tile_index = (tile_y * dispatch_config.tile_count_x) + tile_x; - let tile_range = tile_ranges[tile_index]; + let tile_command_start = tile_starts[tile_index]; + let tile_command_count = atomicLoad(&tile_counts[tile_index]); let dest_x = i32(global_id.x); let dest_y = i32(global_id.y); @@ -200,21 +201,21 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { var tile_command_offset: u32 = 0u; loop { - if (tile_command_offset >= tile_range.command_count) { + if (tile_command_offset >= tile_command_count) { break; } - let command_index = tile_command_indices[tile_range.command_start + tile_command_offset]; + let command_index = tile_command_indices[tile_command_start + tile_command_offset]; let command = commands[command_index]; - let command_min_x = i32(command.destination_x); - let command_min_y = i32(command.destination_y); + let command_min_x = bitcast(command.destination_x); + let command_min_y = bitcast(command.destination_y); let command_max_x = command_min_x + i32(command.destination_width); let command_max_y = command_min_y + i32(command.destination_height); if (dest_x >= command_min_x && dest_x < command_max_x && dest_y >= command_min_y && dest_y < command_max_y) { let local_x = dest_x - command_min_x; let local_y = dest_y - command_min_y; - let coverage_x = i32(command.coverage_offset_x) + local_x; - let coverage_y = i32(command.coverage_offset_y) + local_y; + let coverage_x = bitcast(command.coverage_offset_x) + local_x; + let coverage_y = bitcast(command.coverage_offset_y) + local_y; let coverage_value = textureLoad(coverage_texture, vec2(coverage_x, coverage_y), 0).x; if (coverage_value > 0.0) { let blend_percentage = u32_to_f32(command.blend_percentage); @@ -227,8 +228,8 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { u32_to_f32(command.solid_a)); if (command.brush_type == 1u) { - let origin_x = i32(command.brush_origin_x); - let origin_y = i32(command.brush_origin_y); + let origin_x = bitcast(command.brush_origin_x); + let origin_y = bitcast(command.brush_origin_y); let region_x = i32(command.brush_region_x); let region_y = i32(command.brush_region_y); let region_w = i32(command.brush_region_width); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs new file mode 100644 index 000000000..b0e9fc380 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeTileCountComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct Params { + destination_x: u32, + destination_y: u32, + destination_width: u32, + destination_height: u32, + coverage_offset_x: u32, + coverage_offset_y: u32, + target_width: u32, + brush_type: u32, + brush_origin_x: u32, + brush_origin_y: u32, + brush_region_x: u32, + brush_region_y: u32, + brush_region_width: u32, + brush_region_height: u32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + solid_r: u32, + solid_g: u32, + solid_b: u32, + solid_a: u32, + }; + + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + pad0: u32, + pad1: u32, + }; + + @group(0) @binding(0) var commands: array; + @group(0) @binding(1) var tile_counts: array>; + @group(0) @binding(2) var dispatch_config: DispatchConfig; + + @compute @workgroup_size(64, 1, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + let command_index = global_id.x; + if (command_index >= dispatch_config.command_count) { + return; + } + + if (dispatch_config.tile_count_x == 0u || dispatch_config.tile_count_y == 0u) { + return; + } + + let command = commands[command_index]; + if (command.destination_width == 0u || command.destination_height == 0u) { + return; + } + + let destination_x = bitcast(command.destination_x); + let destination_y = bitcast(command.destination_y); + let destination_max_x = destination_x + i32(command.destination_width) - 1; + let destination_max_y = destination_y + i32(command.destination_height) - 1; + let min_tile_x = u32(max(0, destination_x / 16)); + let min_tile_y = u32(max(0, destination_y / 16)); + let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); + let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); + + if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { + return; + } + + var tile_y = min_tile_y; + loop { + if (tile_y > max_tile_y) { + break; + } + + let row_offset = tile_y * dispatch_config.tile_count_x; + var tile_x = min_tile_x; + loop { + if (tile_x > max_tile_x) { + break; + } + + let tile_index = row_offset + tile_x; + _ = atomicAdd(&tile_counts[tile_index], 1u); + tile_x += 1u; + } + + tile_y += 1u; + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs new file mode 100644 index 000000000..4e3734cd0 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeTilePrefixComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + pad0: u32, + pad1: u32, + }; + + @group(0) @binding(0) var tile_counts: array>; + @group(0) @binding(1) var tile_starts: array; + @group(0) @binding(2) var dispatch_config: DispatchConfig; + + @compute @workgroup_size(1, 1, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + if (global_id.x != 0u || global_id.y != 0u || global_id.z != 0u) { + return; + } + + var running: u32 = 0u; + var tile_index: u32 = 0u; + loop { + if (tile_index >= dispatch_config.tile_count) { + break; + } + + tile_starts[tile_index] = running; + running = running + atomicLoad(&tile_counts[tile_index]); + tile_index += 1u; + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs new file mode 100644 index 000000000..db7e2a396 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeTileScatterComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct Params { + destination_x: u32, + destination_y: u32, + destination_width: u32, + destination_height: u32, + coverage_offset_x: u32, + coverage_offset_y: u32, + target_width: u32, + brush_type: u32, + brush_origin_x: u32, + brush_origin_y: u32, + brush_region_x: u32, + brush_region_y: u32, + brush_region_width: u32, + brush_region_height: u32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + solid_r: u32, + solid_g: u32, + solid_b: u32, + solid_a: u32, + }; + + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + pad0: u32, + pad1: u32, + }; + + @group(0) @binding(0) var commands: array; + @group(0) @binding(1) var tile_starts: array; + @group(0) @binding(2) var tile_write_offsets: array>; + @group(0) @binding(3) var tile_command_indices: array; + @group(0) @binding(4) var dispatch_config: DispatchConfig; + + @compute @workgroup_size(1, 1, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + if (global_id.x != 0u || global_id.y != 0u || global_id.z != 0u) { + return; + } + + if (dispatch_config.tile_count_x == 0u || dispatch_config.tile_count_y == 0u) { + return; + } + + var command_index: u32 = 0u; + loop { + if (command_index >= dispatch_config.command_count) { + break; + } + + let command = commands[command_index]; + if (command.destination_width == 0u || command.destination_height == 0u) { + command_index += 1u; + continue; + } + + let destination_x = bitcast(command.destination_x); + let destination_y = bitcast(command.destination_y); + let destination_max_x = destination_x + i32(command.destination_width) - 1; + let destination_max_y = destination_y + i32(command.destination_height) - 1; + let min_tile_x = u32(max(0, destination_x / 16)); + let min_tile_y = u32(max(0, destination_y / 16)); + let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); + let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); + + if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { + command_index += 1u; + continue; + } + + var tile_y = min_tile_y; + loop { + if (tile_y > max_tile_y) { + break; + } + + let row_offset = tile_y * dispatch_config.tile_count_x; + var tile_x = min_tile_x; + loop { + if (tile_x > max_tile_x) { + break; + } + + let tile_index = row_offset + tile_x; + let local_offset = atomicAdd(&tile_write_offsets[tile_index], 1u); + let write_index = tile_starts[tile_index] + local_offset; + tile_command_indices[write_index] = command_index; + tile_x += 1u; + } + + tile_y += 1u; + } + + command_index += 1u; + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index d4b4b46f2..c7b00e7b4 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -44,11 +44,14 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private const int CompositeComputeWorkgroupSize = 8; private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; + private const int CompositeTileCommandWorkgroupSize = 64; private const int CompositeDestinationPixelStride = 16; private const uint PreparedBrushTypeSolid = 0; private const uint PreparedBrushTypeImage = 1; private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; - private const string PreparedCompositeTileRangesBufferKey = "prepared-composite/tile-ranges"; + private const string PreparedCompositeTileCountsBufferKey = "prepared-composite/tile-counts"; + private const string PreparedCompositeTileStartsBufferKey = "prepared-composite/tile-starts"; + private const string PreparedCompositeTileWriteOffsetsBufferKey = "prepared-composite/tile-write-offsets"; private const string PreparedCompositeTileIndicesBufferKey = "prepared-composite/tile-indices"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; private const int CallbackTimeoutMilliseconds = 10_000; @@ -550,14 +553,6 @@ destinationPixelsBuffer is not null && return false; } - List compositeCommands = new(pendingCommands.Count); - for (int i = 0; i < pendingCommands.Count; i++) - { - PreparedCompositePendingCommand pending = pendingCommands[i]; - CoveragePlacement coveragePlacement = coveragePlacements[pending.CoverageDefinitionIndex]; - compositeCommands.Add(new PreparedCompositeWorkItem(pending.Command, coveragePlacement.OriginX, coveragePlacement.OriginY, (nint)coverageView)); - } - if (!this.TryDispatchPreparedCompositeCommands( flushContext, sourceTextureView, @@ -565,7 +560,9 @@ destinationPixelsBuffer is not null && destinationPixelsByteSize, targetBounds, targetLocalBounds, - compositeCommands, + pendingCommands, + coveragePlacements, + coverageView, out error)) { return false; @@ -592,12 +589,14 @@ private bool TryDispatchPreparedCompositeCommands( nuint destinationPixelsByteSize, Rectangle targetBounds, Rectangle targetLocalBounds, - IReadOnlyList compositeCommands, + IReadOnlyList flushCommands, + CoveragePlacement[] coveragePlacements, + TextureView* coverageTextureView, out string? error) where TPixel : unmanaged, IPixel { error = null; - if (compositeCommands.Count == 0) + if (flushCommands.Count == 0) { return true; } @@ -623,24 +622,19 @@ private bool TryDispatchPreparedCompositeCommands( uint parameterSize = (uint)Unsafe.SizeOf(); IMemoryOwner parametersOwner = - flushContext.MemoryAllocator.Allocate(compositeCommands.Count); - IMemoryOwner validTileCommandsOwner = - flushContext.MemoryAllocator.Allocate(compositeCommands.Count); + flushContext.MemoryAllocator.Allocate(flushCommands.Count); try { - Span parameters = parametersOwner.Memory.Span[..compositeCommands.Count]; - Span validTileCommands = validTileCommandsOwner.Memory.Span[..compositeCommands.Count]; + Span parameters = parametersOwner.Memory.Span[..flushCommands.Count]; TextureView* sourceTextureView = defaultBrushTextureView; nint sourceTextureViewHandle = (nint)defaultBrushTextureView; bool hasImageTexture = false; - nint coverageTextureViewHandle = 0; - bool hasCoverageTexture = false; uint validCommandCount = 0; - for (int i = 0; i < compositeCommands.Count; i++) + for (int i = 0; i < flushCommands.Count; i++) { - PreparedCompositeWorkItem workItem = compositeCommands[i]; - PreparedCompositionCommand command = workItem.Command; + PreparedCompositePendingCommand pendingCommand = flushCommands[i]; + PreparedCompositionCommand command = pendingCommand.Command; if (command.DestinationRegion.Width <= 0 || command.DestinationRegion.Height <= 0) { continue; @@ -701,37 +695,17 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - if (!hasCoverageTexture) - { - coverageTextureViewHandle = workItem.CoverageTextureView; - hasCoverageTexture = true; - } - else if (coverageTextureViewHandle != workItem.CoverageTextureView) - { - error = "Prepared composite flush requires a shared coverage texture."; - return false; - } + CoveragePlacement coveragePlacement = coveragePlacements[pendingCommand.CoverageDefinitionIndex]; int destinationX = command.DestinationRegion.X - targetLocalBounds.X; int destinationY = command.DestinationRegion.Y - targetLocalBounds.Y; - int destinationMaxX = destinationX + command.DestinationRegion.Width - 1; - int destinationMaxY = destinationY + command.DestinationRegion.Height - 1; - int minTileX = Math.Max(0, destinationX / CompositeTileWidth); - int minTileY = Math.Max(0, destinationY / CompositeTileHeight); - int maxTileX = Math.Min(tileCountX - 1, destinationMaxX / CompositeTileWidth); - int maxTileY = Math.Min(tileCountY - 1, destinationMaxY / CompositeTileHeight); - if (maxTileX < minTileX || maxTileY < minTileY) - { - continue; - } - PreparedCompositeParameters commandParameters = new( destinationX, destinationY, command.DestinationRegion.Width, command.DestinationRegion.Height, - command.SourceOffset.X + workItem.CoverageOriginX, - command.SourceOffset.Y + workItem.CoverageOriginY, + command.SourceOffset.X + coveragePlacement.OriginX, + command.SourceOffset.Y + coveragePlacement.OriginY, targetLocalBounds.Width, brushType, brushOriginX, @@ -748,13 +722,6 @@ private bool TryDispatchPreparedCompositeCommands( uint parameterIndex = validCommandCount; parameters[(int)parameterIndex] = commandParameters; - validTileCommands[(int)parameterIndex] = new PreparedCompositeTileCommand( - parameterIndex, - minTileX, - minTileY, - maxTileX, - maxTileY); - validCommandCount++; } @@ -764,72 +731,6 @@ private bool TryDispatchPreparedCompositeCommands( return true; } - if (!hasCoverageTexture) - { - error = "Prepared composite flush did not produce a coverage texture."; - return false; - } - - using IMemoryOwner tileCommandCountsOwner = flushContext.MemoryAllocator.Allocate(tileCount); - using IMemoryOwner tileCommandStartsOwner = flushContext.MemoryAllocator.Allocate(tileCount); - using IMemoryOwner tileCommandWriteOffsetsOwner = flushContext.MemoryAllocator.Allocate(tileCount); - Span tileCommandCounts = tileCommandCountsOwner.Memory.Span[..tileCount]; - Span tileCommandStarts = tileCommandStartsOwner.Memory.Span[..tileCount]; - Span tileCommandWriteOffsets = tileCommandWriteOffsetsOwner.Memory.Span[..tileCount]; - tileCommandCounts.Clear(); - - for (int commandIndex = 0; commandIndex < validCommandCount; commandIndex++) - { - PreparedCompositeTileCommand command = validTileCommands[commandIndex]; - for (int tileY = command.MinTileY; tileY <= command.MaxTileY; tileY++) - { - int rowOffset = checked(tileY * tileCountX); - for (int tileX = command.MinTileX; tileX <= command.MaxTileX; tileX++) - { - tileCommandCounts[rowOffset + tileX]++; - } - } - } - - int totalTileCommandIndices = 0; - for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) - { - tileCommandStarts[tileIndex] = totalTileCommandIndices; - totalTileCommandIndices = checked(totalTileCommandIndices + tileCommandCounts[tileIndex]); - } - - if (totalTileCommandIndices == 0) - { - error = null; - return true; - } - - tileCommandStarts.CopyTo(tileCommandWriteOffsets); - using IMemoryOwner tileCommandIndicesOwner = flushContext.MemoryAllocator.Allocate(totalTileCommandIndices); - Span tileCommandIndices = tileCommandIndicesOwner.Memory.Span[..totalTileCommandIndices]; - - for (int commandIndex = 0; commandIndex < validCommandCount; commandIndex++) - { - PreparedCompositeTileCommand command = validTileCommands[commandIndex]; - for (int tileY = command.MinTileY; tileY <= command.MaxTileY; tileY++) - { - int rowOffset = checked(tileY * tileCountX); - for (int tileX = command.MinTileX; tileX <= command.MaxTileX; tileX++) - { - int tileIndex = rowOffset + tileX; - int writeOffset = tileCommandWriteOffsets[tileIndex]++; - tileCommandIndices[writeOffset] = command.ParameterIndex; - } - } - } - - using IMemoryOwner tileRangesOwner = flushContext.MemoryAllocator.Allocate(tileCount); - Span tileRanges = tileRangesOwner.Memory.Span[..tileCount]; - for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) - { - tileRanges[tileIndex] = new PreparedCompositeTileRange((uint)tileCommandStarts[tileIndex], (uint)tileCommandCounts[tileIndex]); - } - int usedParameterByteCount = checked((int)(validCommandCount * parameterSize)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeParamsBufferKey, @@ -853,48 +754,64 @@ private bool TryDispatchPreparedCompositeCommands( (nuint)usedParameterByteCount); } - int tileRangesByteCount = checked(tileCount * Unsafe.SizeOf()); + int tileCountsByteCount = checked(tileCount * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileRangesBufferKey, + PreparedCompositeTileCountsBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileRangesByteCount, - out WgpuBuffer* tileRangesBuffer, + (nuint)tileCountsByteCount, + out WgpuBuffer* tileCountsBuffer, out _, out error)) { return false; } - fixed (PreparedCompositeTileRange* tileRangesPtr = tileRanges) + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileCountsBuffer, + 0, + (nuint)tileCountsByteCount); + + int tileStartsByteCount = checked(tileCount * sizeof(uint)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeTileStartsBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileStartsByteCount, + out WgpuBuffer* tileStartsBuffer, + out _, + out error)) { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileRangesBuffer, - 0, - tileRangesPtr, - (nuint)tileRangesByteCount); + return false; } - int tileCommandIndicesByteCount = checked(totalTileCommandIndices * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileIndicesBufferKey, + PreparedCompositeTileWriteOffsetsBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileCommandIndicesByteCount, - out WgpuBuffer* tileCommandIndicesBuffer, + (nuint)tileStartsByteCount, + out WgpuBuffer* tileWriteOffsetsBuffer, out _, out error)) { return false; } - fixed (uint* tileCommandIndicesPtr = tileCommandIndices) + nuint maxTileCommandIndices = checked((nuint)validCommandCount * (nuint)tileCount); + if (maxTileCommandIndices == 0) { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileCommandIndicesBuffer, - 0, - tileCommandIndicesPtr, - (nuint)tileCommandIndicesByteCount); + error = null; + return true; + } + + nuint tileCommandIndicesByteCount = checked(maxTileCommandIndices * (nuint)sizeof(uint)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeTileIndicesBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + tileCommandIndicesByteCount, + out WgpuBuffer* tileCommandIndicesBuffer, + out _, + out error)) + { + return false; } nuint dispatchConfigSize = (nuint)Unsafe.SizeOf(); @@ -912,7 +829,10 @@ private bool TryDispatchPreparedCompositeCommands( PreparedCompositeDispatchConfig dispatchConfig = new( (uint)targetLocalBounds.Width, (uint)targetLocalBounds.Height, - (uint)tileCountX); + (uint)tileCountX, + (uint)tileCountY, + (uint)tileCount, + validCommandCount); flushContext.Api.QueueWriteBuffer( flushContext.Queue, dispatchConfigBuffer, @@ -920,11 +840,51 @@ private bool TryDispatchPreparedCompositeCommands( &dispatchConfig, dispatchConfigSize); - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[7]; + if (!this.DispatchPreparedCompositeTileCount( + flushContext, + paramsBuffer, + tileCountsBuffer, + dispatchConfigBuffer, + validCommandCount, + out error)) + { + return false; + } + + if (!this.DispatchPreparedCompositeTilePrefix( + flushContext, + tileCountsBuffer, + tileStartsBuffer, + dispatchConfigBuffer, + out error)) + { + return false; + } + + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileWriteOffsetsBuffer, + 0, + (nuint)tileStartsByteCount); + + if (!this.DispatchPreparedCompositeTileScatter( + flushContext, + paramsBuffer, + tileStartsBuffer, + tileWriteOffsetsBuffer, + tileCommandIndicesBuffer, + dispatchConfigBuffer, + validCommandCount, + out error)) + { + return false; + } + + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, - TextureView = (TextureView*)coverageTextureViewHandle + TextureView = coverageTextureView }; bindGroupEntries[1] = new BindGroupEntry { @@ -948,20 +908,27 @@ private bool TryDispatchPreparedCompositeCommands( bindGroupEntries[4] = new BindGroupEntry { Binding = 4, - Buffer = tileRangesBuffer, + Buffer = tileStartsBuffer, Offset = 0, - Size = (nuint)tileRangesByteCount + Size = (nuint)tileStartsByteCount }; bindGroupEntries[5] = new BindGroupEntry { Binding = 5, - Buffer = tileCommandIndicesBuffer, + Buffer = tileCountsBuffer, Offset = 0, - Size = (nuint)tileCommandIndicesByteCount + Size = (nuint)tileCountsByteCount }; bindGroupEntries[6] = new BindGroupEntry { Binding = 6, + Buffer = tileCommandIndicesBuffer, + Offset = 0, + Size = tileCommandIndicesByteCount + }; + bindGroupEntries[7] = new BindGroupEntry + { + Binding = 7, Buffer = dispatchConfigBuffer, Offset = 0, Size = dispatchConfigSize @@ -970,7 +937,7 @@ private bool TryDispatchPreparedCompositeCommands( BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 7, + EntryCount = 8, Entries = bindGroupEntries }; @@ -1009,13 +976,89 @@ private bool TryDispatchPreparedCompositeCommands( finally { parametersOwner.Dispose(); - validTileCommandsOwner.Dispose(); } error = null; return true; } + private bool DispatchPreparedCompositeTileCount( + WebGPUFlushContext flushContext, + WgpuBuffer* paramsBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* dispatchConfigBuffer, + uint commandCount, + out string? error) + => this.DispatchComputePass( + flushContext, + "prepared-composite-tile-count", + PreparedCompositeTileCountComputeShader.Code, + TryCreatePreparedCompositeTileCountBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = paramsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 3; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( + pass, + DivideRoundUp(checked((int)commandCount), CompositeTileCommandWorkgroupSize), + 1, + 1), + out error); + + private bool DispatchPreparedCompositeTilePrefix( + WebGPUFlushContext flushContext, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* tileStartsBuffer, + WgpuBuffer* dispatchConfigBuffer, + out string? error) + => this.DispatchComputePass( + flushContext, + "prepared-composite-tile-prefix", + PreparedCompositeTilePrefixComputeShader.Code, + TryCreatePreparedCompositeTilePrefixBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 3; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), + out error); + + private bool DispatchPreparedCompositeTileScatter( + WebGPUFlushContext flushContext, + WgpuBuffer* paramsBuffer, + WgpuBuffer* tileStartsBuffer, + WgpuBuffer* tileWriteOffsetsBuffer, + WgpuBuffer* tileCommandIndicesBuffer, + WgpuBuffer* dispatchConfigBuffer, + uint commandCount, + out string? error) + => this.DispatchComputePass( + flushContext, + "prepared-composite-tile-scatter", + PreparedCompositeTileScatterComputeShader.Code, + TryCreatePreparedCompositeTileScatterBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = paramsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileWriteOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 5; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( + pass, + DivideRoundUp(checked((int)commandCount), CompositeTileCommandWorkgroupSize), + 1, + 1), + out error); + private static bool TryGetOrCreateImageTextureView( WebGPUFlushContext flushContext, Image image, @@ -1452,7 +1495,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1514,7 +1557,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, + Type = BufferBindingType.Storage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1524,6 +1567,17 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Binding = 6, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[7] = new BindGroupLayoutEntry + { + Binding = 7, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1533,7 +1587,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, + EntryCount = 8, Entries = entries }; @@ -1548,6 +1602,202 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( return true; } + private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-count bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-prefix bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 5, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-scatter bind group layout."; + return false; + } + + error = null; + return true; + } + /// /// Creates one transient composition texture that can be rendered to, sampled from, and copied. /// @@ -1982,25 +2232,6 @@ public CompositeDestinationBlitParameters( } } - private readonly struct PreparedCompositeWorkItem - { - public PreparedCompositeWorkItem(in PreparedCompositionCommand command, int coverageOriginX, int coverageOriginY, nint coverageTextureView) - { - this.Command = command; - this.CoverageOriginX = coverageOriginX; - this.CoverageOriginY = coverageOriginY; - this.CoverageTextureView = coverageTextureView; - } - - public PreparedCompositionCommand Command { get; } - - public int CoverageOriginX { get; } - - public int CoverageOriginY { get; } - - public nint CoverageTextureView { get; } - } - private readonly struct PreparedCompositePendingCommand { public PreparedCompositePendingCommand(int coverageDefinitionIndex, in PreparedCompositionCommand command) @@ -2033,55 +2264,34 @@ public CoveragePlacement(int originX, int originY, int width, int height) public int Height { get; } } - [StructLayout(LayoutKind.Sequential)] - private readonly struct PreparedCompositeTileRange - { - public readonly uint CommandStart; - public readonly uint CommandCount; - - public PreparedCompositeTileRange(uint commandStart, uint commandCount) - { - this.CommandStart = commandStart; - this.CommandCount = commandCount; - } - } - - private readonly struct PreparedCompositeTileCommand - { - public PreparedCompositeTileCommand(uint parameterIndex, int minTileX, int minTileY, int maxTileX, int maxTileY) - { - this.ParameterIndex = parameterIndex; - this.MinTileX = minTileX; - this.MinTileY = minTileY; - this.MaxTileX = maxTileX; - this.MaxTileY = maxTileY; - } - - public uint ParameterIndex { get; } - - public int MinTileX { get; } - - public int MinTileY { get; } - - public int MaxTileX { get; } - - public int MaxTileY { get; } - } - [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeDispatchConfig { public readonly uint TargetWidth; public readonly uint TargetHeight; public readonly uint TileCountX; + public readonly uint TileCountY; + public readonly uint TileCount; + public readonly uint CommandCount; public readonly uint Pad0; + public readonly uint Pad1; - public PreparedCompositeDispatchConfig(uint targetWidth, uint targetHeight, uint tileCountX) + public PreparedCompositeDispatchConfig( + uint targetWidth, + uint targetHeight, + uint tileCountX, + uint tileCountY, + uint tileCount, + uint commandCount) { this.TargetWidth = targetWidth; this.TargetHeight = targetHeight; this.TileCountX = tileCountX; + this.TileCountY = tileCountY; + this.TileCount = tileCount; + this.CommandCount = commandCount; this.Pad0 = 0; + this.Pad1 = 0; } } From d2d614393c2bc5b6e958e9186dd1d61bf4d3f9e6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 28 Feb 2026 13:41:27 +1000 Subject: [PATCH 028/136] Replace PreparedComposite with fine tiled pipeline --- .../Shaders/PreparedCompositeComputeShader.cs | 260 ---- .../PreparedCompositeFineComputeShader.cs | 527 +++++++ ...PreparedCompositeTileCountComputeShader.cs | 9 +- ...reparedCompositeTilePrefixComputeShader.cs | 6 +- ...eparedCompositeTileScatterComputeShader.cs | 90 +- .../PreparedCompositeTileSortComputeShader.cs | 75 + .../WEBGPU_BACKEND_PROCESS.md | 57 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 1212 ++++++----------- .../WebGPUDrawingBackend.cs | 999 ++++++-------- .../WebGPUFlushContext.cs | 25 - .../WebGPUTestNativeSurfaceAllocator.cs | 2 +- 11 files changed, 1471 insertions(+), 1791 deletions(-) delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs deleted file mode 100644 index 6c0de6708..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeComputeShader.cs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Null-terminated WGSL compute shader for prepared composition batches. -/// -internal static class PreparedCompositeComputeShader -{ - private static readonly byte[] CodeBytes = - [ - .. - """ - struct Params { - destination_x: u32, - destination_y: u32, - destination_width: u32, - destination_height: u32, - coverage_offset_x: u32, - coverage_offset_y: u32, - target_width: u32, - brush_type: u32, - brush_origin_x: u32, - brush_origin_y: u32, - brush_region_x: u32, - brush_region_y: u32, - brush_region_width: u32, - brush_region_height: u32, - color_blend_mode: u32, - alpha_composition_mode: u32, - blend_percentage: u32, - solid_r: u32, - solid_g: u32, - solid_b: u32, - solid_a: u32, - }; - - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - pad0: u32, - pad1: u32, - }; - - @group(0) @binding(0) var coverage_texture: texture_2d; - @group(0) @binding(1) var source_texture: texture_2d; - @group(0) @binding(2) var destination_pixels: array>; - @group(0) @binding(3) var commands: array; - @group(0) @binding(4) var tile_starts: array; - @group(0) @binding(5) var tile_counts: array>; - @group(0) @binding(6) var tile_command_indices: array; - @group(0) @binding(7) var dispatch_config: DispatchConfig; - - fn u32_to_f32(bits: u32) -> f32 { - return bitcast(bits); - } - - fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { - if (alpha <= 0.0) { - return vec3(0.0); - } - - return rgb / alpha; - } - - fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { - switch mode { - case 1u: { - return backdrop * source; - } - case 2u: { - return backdrop + source; - } - case 3u: { - return backdrop - source; - } - case 4u: { - return 1.0 - ((1.0 - backdrop) * (1.0 - source)); - } - case 5u: { - return min(backdrop, source); - } - case 6u: { - return max(backdrop, source); - } - case 7u: { - return select( - 2.0 * backdrop * source, - 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), - backdrop >= vec3(0.5)); - } - case 8u: { - return select( - 2.0 * backdrop * source, - 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), - source >= vec3(0.5)); - } - default: { - return source; - } - } - } - - fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { - let destination_alpha = destination_premul.a; - let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); - let source_alpha = source.a; - let source_rgb = source.rgb; - let source_premul = source_rgb * source_alpha; - let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); - let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); - let shared_alpha = source_alpha * destination_alpha; - - switch alpha_mode { - case 1u: { - return vec4(source_premul, source_alpha); - } - case 2u: { - let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); - return vec4(premul, destination_alpha); - } - case 3u: { - let alpha = source_alpha * destination_alpha; - return vec4(source_premul * destination_alpha, alpha); - } - case 4u: { - let alpha = source_alpha * (1.0 - destination_alpha); - return vec4(source_premul * (1.0 - destination_alpha), alpha); - } - case 5u: { - return destination_premul; - } - case 6u: { - let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); - return vec4(premul, source_alpha); - } - case 7u: { - let alpha = destination_alpha + source_alpha - shared_alpha; - let premul = - (source_rgb * (source_alpha - shared_alpha)) + - (destination_rgb_straight * (destination_alpha - shared_alpha)) + - (reverse_blend * shared_alpha); - return vec4(premul, alpha); - } - case 8u: { - let alpha = destination_alpha * source_alpha; - return vec4(destination_premul.rgb * source_alpha, alpha); - } - case 9u: { - let alpha = destination_alpha * (1.0 - source_alpha); - return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); - } - case 10u: { - return vec4(0.0, 0.0, 0.0, 0.0); - } - case 11u: { - let source_term = source_premul * (1.0 - destination_alpha); - let destination_term = destination_premul.rgb * (1.0 - source_alpha); - let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); - return vec4(source_term + destination_term, alpha); - } - default: { - let alpha = source_alpha + destination_alpha - shared_alpha; - let premul = - (destination_rgb_straight * (destination_alpha - shared_alpha)) + - (source_rgb * (source_alpha - shared_alpha)) + - (forward_blend * shared_alpha); - return vec4(premul, alpha); - } - } - } - - fn positive_mod(value: i32, divisor: i32) -> i32 { - let m = value % divisor; - return select(m + divisor, m, m >= 0); - } - - @compute @workgroup_size(8, 8, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - if (global_id.x >= dispatch_config.target_width || global_id.y >= dispatch_config.target_height) { - return; - } - - let tile_width: u32 = 16u; - let tile_height: u32 = 16u; - let tile_x = global_id.x / tile_width; - let tile_y = global_id.y / tile_height; - let tile_index = (tile_y * dispatch_config.tile_count_x) + tile_x; - let tile_command_start = tile_starts[tile_index]; - let tile_command_count = atomicLoad(&tile_counts[tile_index]); - - let dest_x = i32(global_id.x); - let dest_y = i32(global_id.y); - let dest_index = (global_id.y * dispatch_config.target_width) + global_id.x; - var destination = destination_pixels[dest_index]; - - var tile_command_offset: u32 = 0u; - loop { - if (tile_command_offset >= tile_command_count) { - break; - } - - let command_index = tile_command_indices[tile_command_start + tile_command_offset]; - let command = commands[command_index]; - let command_min_x = bitcast(command.destination_x); - let command_min_y = bitcast(command.destination_y); - let command_max_x = command_min_x + i32(command.destination_width); - let command_max_y = command_min_y + i32(command.destination_height); - if (dest_x >= command_min_x && dest_x < command_max_x && dest_y >= command_min_y && dest_y < command_max_y) { - let local_x = dest_x - command_min_x; - let local_y = dest_y - command_min_y; - let coverage_x = bitcast(command.coverage_offset_x) + local_x; - let coverage_y = bitcast(command.coverage_offset_y) + local_y; - let coverage_value = textureLoad(coverage_texture, vec2(coverage_x, coverage_y), 0).x; - if (coverage_value > 0.0) { - let blend_percentage = u32_to_f32(command.blend_percentage); - let effective_coverage = coverage_value * blend_percentage; - - var brush = vec4( - u32_to_f32(command.solid_r), - u32_to_f32(command.solid_g), - u32_to_f32(command.solid_b), - u32_to_f32(command.solid_a)); - - if (command.brush_type == 1u) { - let origin_x = bitcast(command.brush_origin_x); - let origin_y = bitcast(command.brush_origin_y); - let region_x = i32(command.brush_region_x); - let region_y = i32(command.brush_region_y); - let region_w = i32(command.brush_region_width); - let region_h = i32(command.brush_region_height); - let src_x = positive_mod(dest_x - origin_x, region_w) + region_x; - let src_y = positive_mod(dest_y - origin_y, region_h) + region_y; - brush = textureLoad(source_texture, vec2(src_x, src_y), 0); - } - - let source = vec4(brush.rgb, brush.a * effective_coverage); - destination = compose_pixel(destination, source, command.color_blend_mode, command.alpha_composition_mode); - } - } - - tile_command_offset = tile_command_offset + 1u; - } - - destination_pixels[dest_index] = destination; - } - """u8, - 0 - ]; - - /// - /// Gets the null-terminated UTF-8 WGSL source bytes. - /// - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs new file mode 100644 index 000000000..2d6d1240f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs @@ -0,0 +1,527 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Text; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeFineComputeShader +{ + private static readonly object CacheSync = new(); + private static readonly Dictionary ShaderCache = new(); + + private static readonly string ShaderTemplate = + """ + struct Params { + destination_x: u32, + destination_y: u32, + destination_width: u32, + destination_height: u32, + coverage_offset_x: u32, + coverage_offset_y: u32, + target_width: u32, + brush_type: u32, + brush_origin_x: u32, + brush_origin_y: u32, + brush_region_x: u32, + brush_region_y: u32, + brush_region_width: u32, + brush_region_height: u32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + solid_r: u32, + solid_g: u32, + solid_b: u32, + solid_a: u32, + tile_emit_offset: u32, + tile_emit_count: u32, + }; + + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + }; + + @group(0) @binding(0) var coverage_texture: texture_2d; + @group(0) @binding(1) var backdrop_texture: texture_2d<__BACKDROP_TEXEL_TYPE__>; + @group(0) @binding(2) var brush_texture: texture_2d<__BACKDROP_TEXEL_TYPE__>; + @group(0) @binding(3) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; + @group(0) @binding(4) var commands: array; + @group(0) @binding(5) var tile_starts: array; + @group(0) @binding(6) var tile_counts: array>; + @group(0) @binding(7) var tile_command_indices: array; + @group(0) @binding(8) var dispatch_config: DispatchConfig; + + fn u32_to_f32(bits: u32) -> f32 { + return bitcast(bits); + } + + __DECODE_TEXEL_FUNCTION__ + + __ENCODE_OUTPUT_FUNCTION__ + + fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { + if (alpha <= 0.0) { + return vec3(0.0); + } + + return rgb / alpha; + } + + fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { + switch mode { + case 1u: { + return backdrop * source; + } + case 2u: { + return backdrop + source; + } + case 3u: { + return backdrop - source; + } + case 4u: { + return 1.0 - ((1.0 - backdrop) * (1.0 - source)); + } + case 5u: { + return min(backdrop, source); + } + case 6u: { + return max(backdrop, source); + } + case 7u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + backdrop >= vec3(0.5)); + } + case 8u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + source >= vec3(0.5)); + } + default: { + return source; + } + } + } + + fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { + let destination_alpha = destination_premul.a; + let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); + let source_alpha = source.a; + let source_rgb = source.rgb; + let source_premul = source_rgb * source_alpha; + let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); + let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); + let shared_alpha = source_alpha * destination_alpha; + + switch alpha_mode { + case 1u: { + return vec4(source_premul, source_alpha); + } + case 2u: { + let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); + return vec4(premul, destination_alpha); + } + case 3u: { + let alpha = source_alpha * destination_alpha; + return vec4(source_premul * destination_alpha, alpha); + } + case 4u: { + let alpha = source_alpha * (1.0 - destination_alpha); + return vec4(source_premul * (1.0 - destination_alpha), alpha); + } + case 5u: { + return destination_premul; + } + case 6u: { + let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); + return vec4(premul, source_alpha); + } + case 7u: { + let alpha = destination_alpha + source_alpha - shared_alpha; + let premul = + (source_rgb * (source_alpha - shared_alpha)) + + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (reverse_blend * shared_alpha); + return vec4(premul, alpha); + } + case 8u: { + let alpha = destination_alpha * source_alpha; + return vec4(destination_premul.rgb * source_alpha, alpha); + } + case 9u: { + let alpha = destination_alpha * (1.0 - source_alpha); + return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); + } + case 10u: { + return vec4(0.0, 0.0, 0.0, 0.0); + } + case 11u: { + let source_term = source_premul * (1.0 - destination_alpha); + let destination_term = destination_premul.rgb * (1.0 - source_alpha); + let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); + return vec4(source_term + destination_term, alpha); + } + default: { + let alpha = source_alpha + destination_alpha - shared_alpha; + let premul = + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (source_rgb * (source_alpha - shared_alpha)) + + (forward_blend * shared_alpha); + return vec4(premul, alpha); + } + } + } + + fn positive_mod(value: i32, divisor: i32) -> i32 { + let m = value % divisor; + return select(m + divisor, m, m >= 0); + } + + @compute @workgroup_size(8, 8, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + let tile_index = global_id.z; + if (tile_index >= dispatch_config.tile_count) { + return; + } + + if (global_id.x >= 16u || global_id.y >= 16u) { + return; + } + + let tile_x = tile_index % dispatch_config.tile_count_x; + let tile_y = tile_index / dispatch_config.tile_count_x; + let dest_x = (tile_x * 16u) + global_id.x; + let dest_y = (tile_y * 16u) + global_id.y; + + if (dest_x >= dispatch_config.target_width || dest_y >= dispatch_config.target_height) { + return; + } + + let source_x = i32(dest_x + dispatch_config.source_origin_x); + let source_y = i32(dest_y + dispatch_config.source_origin_y); + let output_x_i32 = i32(dest_x + dispatch_config.output_origin_x); + let output_y_i32 = i32(dest_y + dispatch_config.output_origin_y); + let source = __LOAD_BACKDROP__; + var destination = vec4(source.rgb * source.a, source.a); + let dest_x_i32 = i32(dest_x); + let dest_y_i32 = i32(dest_y); + + let tile_command_start = tile_starts[tile_index]; + let tile_command_count = atomicLoad(&tile_counts[tile_index]); + var tile_command_offset: u32 = 0u; + loop { + if (tile_command_offset >= tile_command_count) { + break; + } + + let command_index = tile_command_indices[tile_command_start + tile_command_offset]; + let command = commands[command_index]; + let command_min_x = bitcast(command.destination_x); + let command_min_y = bitcast(command.destination_y); + let command_max_x = command_min_x + i32(command.destination_width); + let command_max_y = command_min_y + i32(command.destination_height); + if (dest_x_i32 >= command_min_x && dest_x_i32 < command_max_x && dest_y_i32 >= command_min_y && dest_y_i32 < command_max_y) { + let local_x = dest_x_i32 - command_min_x; + let local_y = dest_y_i32 - command_min_y; + let coverage_x = bitcast(command.coverage_offset_x) + local_x; + let coverage_y = bitcast(command.coverage_offset_y) + local_y; + let coverage_value = textureLoad(coverage_texture, vec2(coverage_x, coverage_y), 0).x; + if (coverage_value > 0.0) { + let blend_percentage = u32_to_f32(command.blend_percentage); + let effective_coverage = coverage_value * blend_percentage; + + var brush = vec4( + u32_to_f32(command.solid_r), + u32_to_f32(command.solid_g), + u32_to_f32(command.solid_b), + u32_to_f32(command.solid_a)); + + if (command.brush_type == 1u) { + let origin_x = bitcast(command.brush_origin_x); + let origin_y = bitcast(command.brush_origin_y); + let region_x = i32(command.brush_region_x); + let region_y = i32(command.brush_region_y); + let region_w = i32(command.brush_region_width); + let region_h = i32(command.brush_region_height); + let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; + let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; + brush = __LOAD_BRUSH__; + } + + let src = vec4(brush.rgb, brush.a * effective_coverage); + destination = compose_pixel(destination, src, command.color_blend_mode, command.alpha_composition_mode); + } + } + + tile_command_offset += 1u; + } + + let alpha = destination.a; + let rgb = unpremultiply(destination.rgb, alpha); + __STORE_OUTPUT__ + } + """; + + public static bool TryGetInputSampleType(TextureFormat textureFormat, out TextureSampleType sampleType) + { + if (TryGetTraits(textureFormat, out ShaderTraits traits)) + { + sampleType = traits.SampleType; + return true; + } + + sampleType = default; + return false; + } + + public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error) + { + if (!TryGetTraits(textureFormat, out ShaderTraits traits)) + { + code = Array.Empty(); + error = $"Prepared composite fine shader does not support texture format '{textureFormat}'."; + return false; + } + + lock (CacheSync) + { + if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode) && cachedCode is not null) + { + code = cachedCode; + error = null; + return true; + } + + string source = ShaderTemplate + .Replace("__BACKDROP_TEXEL_TYPE__", traits.BackdropTexelType, StringComparison.Ordinal) + .Replace("__OUTPUT_FORMAT__", traits.OutputFormat, StringComparison.Ordinal) + .Replace("__DECODE_TEXEL_FUNCTION__", traits.DecodeTexelFunction, StringComparison.Ordinal) + .Replace("__ENCODE_OUTPUT_FUNCTION__", traits.EncodeOutputFunction, StringComparison.Ordinal) + .Replace("__LOAD_BACKDROP__", traits.LoadBackdropExpression, StringComparison.Ordinal) + .Replace("__LOAD_BRUSH__", traits.LoadBrushExpression, StringComparison.Ordinal) + .Replace("__STORE_OUTPUT__", traits.StoreOutputStatement, StringComparison.Ordinal); + + byte[] sourceBytes = Encoding.UTF8.GetBytes(source); + code = new byte[sourceBytes.Length + 1]; + sourceBytes.CopyTo(code, 0); + code[^1] = 0; + ShaderCache[textureFormat] = code; + } + + error = null; + return true; + } + + private static bool TryGetTraits(TextureFormat textureFormat, out ShaderTraits traits) + { + switch (textureFormat) + { + case TextureFormat.R8Unorm: + traits = CreateFloatTraits("r8unorm"); + return true; + case TextureFormat.RG8Unorm: + traits = CreateFloatTraits("rg8unorm"); + return true; + case TextureFormat.Rgba8Unorm: + traits = CreateFloatTraits("rgba8unorm"); + return true; + case TextureFormat.Bgra8Unorm: + traits = CreateFloatTraits("bgra8unorm"); + return true; + case TextureFormat.Rgb10A2Unorm: + traits = CreateFloatTraits("rgb10a2unorm"); + return true; + case TextureFormat.R16float: + traits = CreateFloatTraits("r16float"); + return true; + case TextureFormat.RG16float: + traits = CreateFloatTraits("rg16float"); + return true; + case TextureFormat.Rgba16float: + traits = CreateFloatTraits("rgba16float"); + return true; + case TextureFormat.Rgba32float: + traits = CreateFloatTraits("rgba32float"); + return true; + case TextureFormat.RG8Snorm: + traits = CreateSnormTraits("rg8snorm"); + return true; + case TextureFormat.Rgba8Snorm: + traits = CreateSnormTraits("rgba8snorm"); + return true; + case TextureFormat.Rgba8Uint: + traits = CreateUintTraits("rgba8uint", 255F); + return true; + case TextureFormat.R16Uint: + traits = CreateUintTraits("r16uint", 65535F); + return true; + case TextureFormat.RG16Uint: + traits = CreateUintTraits("rg16uint", 65535F); + return true; + case TextureFormat.Rgba16Uint: + traits = CreateUintTraits("rgba16uint", 65535F); + return true; + case TextureFormat.RG16Sint: + traits = CreateSintTraits("rg16sint", -32768F, 32767F); + return true; + case TextureFormat.Rgba16Sint: + traits = CreateSintTraits("rgba16sint", -32768F, 32767F); + return true; + default: + traits = default; + return false; + } + } + + private static ShaderTraits CreateFloatTraits(string outputFormat) + { + const string DecodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return texel; + } + """; + + const string EncodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + return color; + } + """; + + return new ShaderTraits( + outputFormat, + "f32", + TextureSampleType.Float, + DecodeTexel, + EncodeOutput, + "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", + "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", + "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); + } + + private static ShaderTraits CreateSnormTraits(string outputFormat) + { + const string DecodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return (texel * 0.5) + vec4(0.5); + } + """; + + const string EncodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return (clamped * 2.0) - vec4(1.0); + } + """; + + return new ShaderTraits( + outputFormat, + "f32", + TextureSampleType.Float, + DecodeTexel, + EncodeOutput, + "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", + "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", + "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); + } + + private static ShaderTraits CreateUintTraits(string outputFormat, float maxValue) + { + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + string decodeTexel = $@"const UINT_TEXEL_MAX: vec4 = {maxVector}; +fn decode_texel(texel: vec4) -> vec4 {{ + return vec4(texel) / UINT_TEXEL_MAX; +}}"; + const string EncodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round(clamped * UINT_TEXEL_MAX)); + } + """; + + return new ShaderTraits( + outputFormat, + "u32", + TextureSampleType.Uint, + decodeTexel, + EncodeOutput, + "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", + "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", + "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); + } + + private static ShaderTraits CreateSintTraits(string outputFormat, float minValue, float maxValue) + { + string minVector = $"vec4({minValue:F1}, {minValue:F1}, {minValue:F1}, {minValue:F1})"; + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + string decodeTexel = $@"const SINT_TEXEL_MIN: vec4 = {minVector}; +const SINT_TEXEL_MAX: vec4 = {maxVector}; +const SINT_TEXEL_RANGE: vec4 = SINT_TEXEL_MAX - SINT_TEXEL_MIN; +fn decode_texel(texel: vec4) -> vec4 {{ + return (vec4(texel) - SINT_TEXEL_MIN) / SINT_TEXEL_RANGE; +}}"; + const string EncodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round((clamped * SINT_TEXEL_RANGE) + SINT_TEXEL_MIN)); + } + """; + + return new ShaderTraits( + outputFormat, + "i32", + TextureSampleType.Sint, + decodeTexel, + EncodeOutput, + "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", + "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", + "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); + } + + private readonly struct ShaderTraits( + string outputFormat, + string backdropTexelType, + TextureSampleType sampleType, + string decodeTexelFunction, + string encodeOutputFunction, + string loadBackdropExpression, + string loadBrushExpression, + string storeOutputStatement) + { + public string OutputFormat { get; } = outputFormat; + + public string BackdropTexelType { get; } = backdropTexelType; + + public TextureSampleType SampleType { get; } = sampleType; + + public string DecodeTexelFunction { get; } = decodeTexelFunction; + + public string EncodeOutputFunction { get; } = encodeOutputFunction; + + public string LoadBackdropExpression { get; } = loadBackdropExpression; + + public string LoadBrushExpression { get; } = loadBrushExpression; + + public string StoreOutputStatement { get; } = storeOutputStatement; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs index b0e9fc380..07b771863 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs @@ -31,6 +31,8 @@ struct Params { solid_g: u32, solid_b: u32, solid_a: u32, + tile_emit_offset: u32, + tile_emit_count: u32, }; struct DispatchConfig { @@ -77,21 +79,24 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { return; } + let emit_count = command.tile_emit_count; + var emitted: u32 = 0u; var tile_y = min_tile_y; loop { - if (tile_y > max_tile_y) { + if (tile_y > max_tile_y || emitted >= emit_count) { break; } let row_offset = tile_y * dispatch_config.tile_count_x; var tile_x = min_tile_x; loop { - if (tile_x > max_tile_x) { + if (tile_x > max_tile_x || emitted >= emit_count) { break; } let tile_index = row_offset + tile_x; _ = atomicAdd(&tile_counts[tile_index], 1u); + emitted += 1u; tile_x += 1u; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs index 4e3734cd0..a196ddd7f 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs @@ -37,10 +37,12 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { break; } + let tile_count = atomicLoad(&tile_counts[tile_index]); tile_starts[tile_index] = running; - running = running + atomicLoad(&tile_counts[tile_index]); - tile_index += 1u; + running = running + tile_count; + tile_index = tile_index + 1u; } + } """u8, 0 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs index db7e2a396..ec748f5b2 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs @@ -31,6 +31,8 @@ struct Params { solid_g: u32, solid_b: u32, solid_a: u32, + tile_emit_offset: u32, + tile_emit_count: u32, }; struct DispatchConfig { @@ -40,19 +42,21 @@ struct DispatchConfig { tile_count_y: u32, tile_count: u32, command_count: u32, - pad0: u32, - pad1: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, }; @group(0) @binding(0) var commands: array; - @group(0) @binding(1) var tile_starts: array; - @group(0) @binding(2) var tile_write_offsets: array>; - @group(0) @binding(3) var tile_command_indices: array; - @group(0) @binding(4) var dispatch_config: DispatchConfig; + @group(0) @binding(1) var tile_offsets: array>; + @group(0) @binding(2) var tile_command_indices: array; + @group(0) @binding(3) var dispatch_config: DispatchConfig; - @compute @workgroup_size(1, 1, 1) + @compute @workgroup_size(64, 1, 1) fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - if (global_id.x != 0u || global_id.y != 0u || global_id.z != 0u) { + let command_index = global_id.x; + if (command_index >= dispatch_config.command_count) { return; } @@ -60,56 +64,46 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { return; } - var command_index: u32 = 0u; - loop { - if (command_index >= dispatch_config.command_count) { - break; - } - - let command = commands[command_index]; - if (command.destination_width == 0u || command.destination_height == 0u) { - command_index += 1u; - continue; - } + let command = commands[command_index]; + if (command.destination_width == 0u || command.destination_height == 0u) { + return; + } - let destination_x = bitcast(command.destination_x); - let destination_y = bitcast(command.destination_y); - let destination_max_x = destination_x + i32(command.destination_width) - 1; - let destination_max_y = destination_y + i32(command.destination_height) - 1; - let min_tile_x = u32(max(0, destination_x / 16)); - let min_tile_y = u32(max(0, destination_y / 16)); - let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); - let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); + let destination_x = bitcast(command.destination_x); + let destination_y = bitcast(command.destination_y); + let destination_max_x = destination_x + i32(command.destination_width) - 1; + let destination_max_y = destination_y + i32(command.destination_height) - 1; + let min_tile_x = u32(max(0, destination_x / 16)); + let min_tile_y = u32(max(0, destination_y / 16)); + let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); + let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); + if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { + return; + } - if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { - command_index += 1u; - continue; + let emit_count = command.tile_emit_count; + var emitted: u32 = 0u; + var tile_y = min_tile_y; + loop { + if (tile_y > max_tile_y || emitted >= emit_count) { + break; } - var tile_y = min_tile_y; + let row_offset = tile_y * dispatch_config.tile_count_x; + var tile_x = min_tile_x; loop { - if (tile_y > max_tile_y) { + if (tile_x > max_tile_x || emitted >= emit_count) { break; } - let row_offset = tile_y * dispatch_config.tile_count_x; - var tile_x = min_tile_x; - loop { - if (tile_x > max_tile_x) { - break; - } - - let tile_index = row_offset + tile_x; - let local_offset = atomicAdd(&tile_write_offsets[tile_index], 1u); - let write_index = tile_starts[tile_index] + local_offset; - tile_command_indices[write_index] = command_index; - tile_x += 1u; - } - - tile_y += 1u; + let tile_index = row_offset + tile_x; + let write_index = atomicAdd(&tile_offsets[tile_index], 1u); + tile_command_indices[write_index] = command_index; + emitted += 1u; + tile_x += 1u; } - command_index += 1u; + tile_y += 1u; } } """u8, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs new file mode 100644 index 000000000..2dd8c74c7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeTileSortComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. + """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + }; + + @group(0) @binding(0) var tile_starts: array; + @group(0) @binding(1) var tile_counts: array>; + @group(0) @binding(2) var tile_command_indices: array; + @group(0) @binding(3) var dispatch_config: DispatchConfig; + + @compute @workgroup_size(1, 1, 1) + fn cs_main(@builtin(global_invocation_id) global_id: vec3) { + let tile_index = global_id.x; + if (tile_index >= dispatch_config.tile_count) { + return; + } + + let start = tile_starts[tile_index]; + let count = atomicLoad(&tile_counts[tile_index]); + if (count <= 1u) { + return; + } + + var i: u32 = 1u; + loop { + if (i >= count) { + break; + } + + let key = tile_command_indices[start + i]; + var j: u32 = i; + loop { + if (j == 0u) { + break; + } + + let previous_index = start + j - 1u; + let previous_value = tile_command_indices[previous_index]; + if (previous_value <= key) { + break; + } + + tile_command_indices[start + j] = previous_value; + j = j - 1u; + } + + tile_command_indices[start + j] = key; + i = i + 1u; + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index b8e96fb0a..b4e59def5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -16,29 +16,31 @@ DrawingCanvasBatcher.Flush() -> group contiguous commands by DefinitionKey -> keep prepared destination/source offsets -> acquire one WebGPUFlushContext for the scene - -> for each prepared batch - -> ensure command encoder (single encoder reused for the scene) - -> initialize destination storage buffer once per flush (premultiplied vec4) - -> source = target view when sampleable - -> else copy target region into transient composition texture, then sample that - -> run CompositeDestinationInitShader compute pass - -> build coverage texture from prepared geometry - -> flatten prepared path geometry - -> upload line/path/tile/segment buffers - -> run compute sequence: - 1) PathCountSetup - 2) PathCount - 3) Backdrop - 4) SegmentAlloc - 5) PathTilingSetup - 6) PathTiling - 7) CoverageFine - -> composite commands into destination storage (PreparedCompositeComputeShader) - -> solid brush uses Color.ToScaledVector4() - -> image brush samples Image texture directly - -> blit destination storage back to target (CompositeDestinationBlitShader) - -> render pass uses LoadOp.Load + StoreOp.Store - -> scissor limits writes to destination bounds + -> ensure command encoder (single encoder reused for the scene) + -> resolve source backdrop texture view for composition bounds + -> source = target view when sampleable + -> else copy target region into transient composition texture and sample that + -> build coverage texture from prepared geometry + -> flatten prepared path geometry + -> upload line/path/tile/segment buffers + -> run compute sequence: + 1) PathCountSetup + 2) PathCount + 3) Backdrop + 4) SegmentAlloc + 5) PathTilingSetup + 6) PathTiling + 7) CoverageFine + -> build one flush-scoped composite command stream + -> command-parallel tile count + -> tile prefix + -> command-parallel tile scatter + -> per-tile command index sort (ascending command_index) + -> run one fine composite dispatch (PreparedCompositeFineComputeShader) + -> solid brush uses Color.ToScaledVector4() + -> image brush samples Image texture directly + -> writes composed pixels to one transient output texture + -> copy output texture bounds back into the destination target once -> finalize once -> finish encoder -> single queue submit for the flush context @@ -50,17 +52,14 @@ DrawingCanvasBatcher.Flush() - `WebGPUFlushContext` is created once per `FlushCompositions` execution. - The same command encoder is reused across all batch passes in that flush. -- Destination storage (`CompositeDestinationPixelsBuffer`) is initialized once and reused across batches in the same flush. - Transient textures/buffers/bind-groups are tracked in the flush context and released on dispose. - Source image texture views are cached per flush context to avoid duplicate uploads. ## Destination Writeback and Flush Count - `FlushCompositions` performs one command-buffer submission (`QueueSubmit`) per scene flush. -- Destination writeback to the render target occurs via destination blit pass(es) before final submit: - - one blit per prepared batch in command order, - - with destination storage preserved across batches. -- For scenes that plan to a single prepared batch (common case), this is one destination blit pass. +- Destination writeback to the render target is one copy from the fine output texture into composition bounds. +- No destination storage init/blit pass is used in the active flush path. ## Fallback Behavior @@ -78,4 +77,4 @@ Fallback is scene-scoped: All static WGSL shader sources are stored as null-terminated UTF-8 bytes (`U+0000` terminator at call site requirement), including: - coverage pipeline shaders (`PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine`) -- composition shaders (`PreparedComposite`, `CompositeDestinationInit`, `CompositeDestinationBlit`) +- composition shaders (`PreparedCompositeTileCount`, `PreparedCompositeTilePrefix`, `PreparedCompositeTileScatter`, `PreparedCompositeTileSort`, `PreparedCompositeFine`) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 336c36ded..5141c7e77 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -29,6 +29,8 @@ internal sealed unsafe partial class WebGPUDrawingBackend private const int SegmentStrideBytes = 24; private const int SegmentAllocWorkgroupSize = 256; + private readonly Dictionary coverageGeometryCache = new(); + private delegate uint BindGroupEntryWriter(Span entries); private unsafe delegate void ComputePassDispatch(ComputePassEncoder* pass); @@ -60,865 +62,409 @@ private bool TryCreateCoverageTextureFromFlattened( int currentTileY = 0; uint? fillRuleValue = null; uint? aliasedValue = null; - try + for (int i = 0; i < definitions.Count; i++) { - for (int i = 0; i < definitions.Count; i++) + CompositionCoverageDefinition definition = definitions[i]; + Rectangle interest = definition.RasterizerOptions.Interest; + if (interest.Width <= 0 || interest.Height <= 0) { - CompositionCoverageDefinition definition = definitions[i]; - Rectangle interest = definition.RasterizerOptions.Interest; - if (interest.Width <= 0 || interest.Height <= 0) - { - error = "Invalid coverage bounds."; - return false; - } - - uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; - uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; - if ((fillRuleValue.HasValue && fillRuleValue.Value != fillRule) || - (aliasedValue.HasValue && aliasedValue.Value != isAliased)) - { - error = "Mixed rasterization modes are not supported in one flush coverage pass."; - return false; - } - - fillRuleValue ??= fillRule; - aliasedValue ??= isAliased; - - int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); - int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); - int originTileX = 0; - int originTileY = currentTileY; - int originX = originTileX * TileWidth; - int originY = originTileY * TileHeight; - - if (!TryBuildLineBuffer( - definition.Path, - in interest, - definition.RasterizerOptions.SamplingOrigin, - configuration.MemoryAllocator, - out IMemoryOwner? lineOwner, - out int lineCount, - out _, - out _, - out _, - out _, - out uint estimatedSegments, - out error)) - { - return false; - } - - pathBuilds[i] = new CoveragePathBuild( - lineOwner, - lineCount, - estimatedSegments, - widthInTiles, - heightInTiles, - originTileX, - originTileY, - originX, - originY, - interest.Width, - interest.Height); - coveragePlacements[i] = new CoveragePlacement(originX, originY, interest.Width, interest.Height); - - totalLineCount = checked(totalLineCount + lineCount); - totalEstimatedSegments += estimatedSegments; - atlasWidthInTiles = Math.Max(atlasWidthInTiles, widthInTiles); - atlasHeightInTiles = Math.Max(atlasHeightInTiles, originTileY + heightInTiles); - currentTileY += heightInTiles; + error = "Invalid coverage bounds."; + return false; } - totalTileCount = checked(atlasWidthInTiles * atlasHeightInTiles); - - int atlasWidth = Math.Max(1, atlasWidthInTiles * TileWidth); - int atlasHeight = Math.Max(1, atlasHeightInTiles * TileHeight); - if (!TryCreateCoverageTexture( - flushContext, - atlasWidth, - atlasHeight, - configuration.MemoryAllocator, - totalLineCount == 0, - out Texture* coverageTexture, - out coverageView, - out error)) + uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; + uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; + if ((fillRuleValue.HasValue && fillRuleValue.Value != fillRule) || + (aliasedValue.HasValue && aliasedValue.Value != isAliased)) { + error = "Mixed rasterization modes are not supported in one flush coverage pass."; return false; } - flushContext.TrackTexture(coverageTexture); - flushContext.TrackTextureView(coverageView); - if (totalLineCount == 0) - { - return true; - } + fillRuleValue ??= fillRule; + aliasedValue ??= isAliased; + + int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); + int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); + int originTileX = 0; + int originTileY = currentTileY; + int originX = originTileX * TileWidth; + int originY = originTileY * TileHeight; - int lineBufferBytes = checked(totalLineCount * LineStrideBytes); - using IMemoryOwner lineUploadOwner = configuration.MemoryAllocator.Allocate(lineBufferBytes); - Span lineUpload = lineUploadOwner.Memory.Span[..lineBufferBytes]; - int mergedLineIndex = 0; - for (int pathIndex = 0; pathIndex < pathBuilds.Length; pathIndex++) + CoverageDefinitionIdentity identity = new(definition); + if (!this.coverageGeometryCache.TryGetValue(identity, out CachedCoverageGeometry? geometry)) { - CoveragePathBuild build = pathBuilds[pathIndex]; - if (build.LineCount == 0 || build.LineOwner is null) + IMemoryOwner? lineOwner = null; + try { - continue; + if (!TryBuildLineBuffer( + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + configuration.MemoryAllocator, + out lineOwner, + out int lineCount, + out _, + out _, + out _, + out _, + out uint estimatedSegments, + out error)) + { + return false; + } + + geometry = new CachedCoverageGeometry( + lineOwner, + lineCount, + estimatedSegments, + widthInTiles, + heightInTiles, + interest.Width, + interest.Height); + lineOwner = null; + this.coverageGeometryCache[identity] = geometry; } - - ReadOnlySpan sourceLines = build.LineOwner.Memory.Span[..(build.LineCount * LineStrideBytes)]; - for (int lineIndex = 0; lineIndex < build.LineCount; lineIndex++) + finally { - int sourceOffset = lineIndex * LineStrideBytes; - float x0 = ReadFloat(sourceLines, sourceOffset + 8) + build.OriginX; - float y0 = ReadFloat(sourceLines, sourceOffset + 12) + build.OriginY; - float x1 = ReadFloat(sourceLines, sourceOffset + 16) + build.OriginX; - float y1 = ReadFloat(sourceLines, sourceOffset + 20) + build.OriginY; - WriteLine(lineUpload, mergedLineIndex, (uint)pathIndex, x0, y0, x1, y1); - mergedLineIndex++; + lineOwner?.Dispose(); } } - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-lines", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)lineBufferBytes, - out WgpuBuffer* lineBuffer, - out error)) - { - return false; - } - - fixed (byte* linePtr = lineUpload) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - lineBuffer, - 0, - linePtr, - (nuint)lineBufferBytes); - } - - int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); - using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); - Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; - int tileBase = 0; - for (int i = 0; i < pathBuilds.Length; i++) - { - CoveragePathBuild build = pathBuilds[i]; - WritePath( - pathUpload.Slice(i * PathStrideBytes, PathStrideBytes), - (uint)build.OriginTileX, - (uint)build.OriginTileY, - (uint)(build.OriginTileX + atlasWidthInTiles), - (uint)(build.OriginTileY + build.HeightInTiles), - (uint)tileBase); - tileBase = checked(tileBase + (atlasWidthInTiles * build.HeightInTiles)); - } - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-paths", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)pathBufferBytes, - out WgpuBuffer* pathBuffer, - out error)) + if (geometry is null) { + error = "Failed to resolve cached coverage geometry."; return false; } - fixed (byte* pathPtr = pathUpload) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - pathBuffer, - 0, - pathPtr, - (nuint)pathBufferBytes); - } - - int tileBufferBytes = checked(totalTileCount * TileStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-tiles", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileBufferBytes, - out WgpuBuffer* tileBuffer, - out error)) - { - return false; - } + pathBuilds[i] = new CoveragePathBuild( + geometry, + originTileX, + originTileY, + originX, + originY); + coveragePlacements[i] = new CoveragePlacement(originX, originY, interest.Width, interest.Height); - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileBuffer, - 0, - (nuint)tileBufferBytes); - - int tileCountsBytes = checked(totalTileCount * sizeof(uint)); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-tile-counts", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileCountsBytes, - out WgpuBuffer* tileCountsBuffer, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileCountsBuffer, - 0, - (nuint)tileCountsBytes); - - if (totalEstimatedSegments > int.MaxValue) - { - error = "Coverage segment estimate overflow."; - return false; - } - - uint segCountsCapacity = totalEstimatedSegments == 0 ? 1u : checked((uint)totalEstimatedSegments); - uint segmentsCapacity = segCountsCapacity; - int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segment-counts", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)segCountsBytes, - out WgpuBuffer* segCountsBuffer, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - segCountsBuffer, - 0, - (nuint)segCountsBytes); - - int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segments", - BufferUsage.Storage, - (nuint)segmentsBytes, - out WgpuBuffer* segmentsBuffer, - out error)) - { - return false; - } - - RasterConfig config = new() - { - WidthInTiles = (uint)atlasWidthInTiles, - HeightInTiles = (uint)atlasHeightInTiles, - TargetWidth = (uint)atlasWidth, - TargetHeight = (uint)atlasHeight, - BaseColor = 0, - NDrawObj = 0, - NPath = (uint)pathBuilds.Length, - NClip = 0, - BinDataStart = 0, - PathtagBase = 0, - PathdataBase = 0, - DrawtagBase = 0, - DrawdataBase = 0, - TransformBase = 0, - StyleBase = 0, - LinesSize = (uint)totalLineCount, - BinningSize = (uint)pathBuilds.Length, - TilesSize = (uint)totalTileCount, - SegCountsSize = segCountsCapacity, - SegmentsSize = segmentsCapacity, - BlendSize = 1, - PtclSize = 1 - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-raster-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* configBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, (nuint)Unsafe.SizeOf()); - - BumpAllocatorsData bumpData = new() - { - Failed = 0, - Binning = 0, - Ptcl = 0, - Tile = 0, - SegCounts = 0, - Segments = 0, - Blend = 0, - Lines = (uint)totalLineCount - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-bump", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* bumpBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, bumpBuffer, 0, &bumpData, (nuint)Unsafe.SizeOf()); - - IndirectCountData indirectData = default; - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-indirect", - BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* indirectBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, indirectBuffer, 0, &indirectData, (nuint)Unsafe.SizeOf()); - - SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)totalTileCount }; - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segment-alloc", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* segmentAllocBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, segmentAllocBuffer, 0, &segmentAllocConfig, (nuint)Unsafe.SizeOf()); - - CoverageConfig coverageConfig = new() - { - TargetWidth = (uint)atlasWidth, - TargetHeight = (uint)atlasHeight, - TileOriginX = 0, - TileOriginY = 0, - TileWidthInTiles = (uint)atlasWidthInTiles, - TileHeightInTiles = (uint)atlasHeightInTiles, - FillRule = fillRuleValue.GetValueOrDefault(0), - IsAliased = aliasedValue.GetValueOrDefault(0) - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-coverage-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* coverageConfigBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, coverageConfigBuffer, 0, &coverageConfig, (nuint)Unsafe.SizeOf()); - - if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error) || - !this.DispatchPathCount(flushContext, configBuffer, bumpBuffer, lineBuffer, pathBuffer, tileBuffer, segCountsBuffer, indirectBuffer, out error) || - !this.DispatchBackdrop(flushContext, configBuffer, tileBuffer, atlasHeightInTiles, out error) || - !this.DispatchSegmentAlloc(flushContext, bumpBuffer, tileBuffer, tileCountsBuffer, segmentAllocBuffer, totalTileCount, out error) || - !this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error) || - !this.DispatchPathTiling(flushContext, bumpBuffer, segCountsBuffer, lineBuffer, pathBuffer, tileBuffer, segmentsBuffer, indirectBuffer, out error) || - !this.DispatchCoverageFine(flushContext, coverageConfigBuffer, tileBuffer, tileCountsBuffer, segmentsBuffer, coverageView, atlasWidthInTiles, atlasHeightInTiles, out error)) - { - return false; - } - - error = null; - return true; - } - finally - { - for (int i = 0; i < pathBuilds.Length; i++) - { - pathBuilds[i].LineOwner?.Dispose(); - } + totalLineCount = checked(totalLineCount + geometry.LineCount); + totalEstimatedSegments += geometry.EstimatedSegments; + atlasWidthInTiles = Math.Max(atlasWidthInTiles, geometry.WidthInTiles); + atlasHeightInTiles = Math.Max(atlasHeightInTiles, originTileY + geometry.HeightInTiles); + currentTileY += geometry.HeightInTiles; } - } - private bool TryCreateCoverageTextureFromFlattened( - WebGPUFlushContext flushContext, - in CompositionCoverageDefinition definition, - Configuration configuration, - out TextureView* coverageView, - out string? error) - where TPixel : unmanaged, IPixel - { - coverageView = null; - error = null; - - Rectangle interest = definition.RasterizerOptions.Interest; - if (interest.Width <= 0 || interest.Height <= 0) + totalTileCount = checked(atlasWidthInTiles * atlasHeightInTiles); + + int atlasWidth = Math.Max(1, atlasWidthInTiles * TileWidth); + int atlasHeight = Math.Max(1, atlasHeightInTiles * TileHeight); + if (!TryCreateCoverageTexture( + flushContext, + atlasWidth, + atlasHeight, + configuration.MemoryAllocator, + totalLineCount == 0, + out Texture* coverageTexture, + out coverageView, + out error)) { - error = "Invalid coverage bounds."; return false; } - IMemoryOwner? lineOwner = null; - try + flushContext.TrackTexture(coverageTexture); + flushContext.TrackTextureView(coverageView); + if (totalLineCount == 0) { - if (!TryBuildLineBuffer( - definition.Path, - in interest, - definition.RasterizerOptions.SamplingOrigin, - configuration.MemoryAllocator, - out lineOwner, - out int lineCount, - out float minX, - out float minY, - out float maxX, - out float maxY, - out uint estimatedSegments, - out error)) - { - return false; - } - - if (!TryCreateCoverageTexture( - flushContext, - interest.Width, - interest.Height, - configuration.MemoryAllocator, - lineCount == 0, - out Texture* coverageTexture, - out coverageView, - out error)) - { - return false; - } - - flushContext.TrackTexture(coverageTexture); - flushContext.TrackTextureView(coverageView); - - if (lineCount == 0) - { - return true; - } - - int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); - int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); - int tileMinX = 0; - int tileMinY = 0; - int tileMaxX = widthInTiles; - int tileMaxY = heightInTiles; - - int tileWidth = tileMaxX - tileMinX; - int tileHeight = tileMaxY - tileMinY; - if (tileWidth <= 0 || tileHeight <= 0) - { - return true; - } - - int tileCount = checked(tileWidth * tileHeight); - uint segCountsCapacity = estimatedSegments == 0 ? 1u : estimatedSegments; - uint segmentsCapacity = segCountsCapacity; - - int lineBufferBytes = checked(lineCount * LineStrideBytes); - BufferDescriptor lineDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)lineBufferBytes - }; - - WgpuBuffer* lineBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in lineDescriptor); - if (lineBuffer is null) - { - error = "Failed to create line buffer."; - return false; - } - - flushContext.TrackBuffer(lineBuffer); - if (lineOwner is null) - { - error = "Missing line buffer allocation."; - return false; - } - - Span lineBytes = lineOwner.Memory.Span[..lineBufferBytes]; - fixed (byte* linePtr = lineBytes) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - lineBuffer, - 0, - linePtr, - (nuint)lineBufferBytes); - } - - Span pathBytes = stackalloc byte[PathStrideBytes]; - WritePath(pathBytes, (uint)tileMinX, (uint)tileMinY, (uint)tileMaxX, (uint)tileMaxY); - - BufferDescriptor pathDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)PathStrideBytes - }; - - WgpuBuffer* pathBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in pathDescriptor); - if (pathBuffer is null) - { - error = "Failed to create path buffer."; - return false; - } - - flushContext.TrackBuffer(pathBuffer); - fixed (byte* pathPtr = pathBytes) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - pathBuffer, - 0, - pathPtr, - (nuint)PathStrideBytes); - } - - int tileBufferBytes = checked(tileCount * TileStrideBytes); - BufferDescriptor tileDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)tileBufferBytes - }; - - WgpuBuffer* tileBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in tileDescriptor); - if (tileBuffer is null) - { - error = "Failed to create tile buffer."; - return false; - } - - flushContext.TrackBuffer(tileBuffer); - using (IMemoryOwner tileZeroOwner = configuration.MemoryAllocator.Allocate(tileBufferBytes)) - { - Span tileZero = tileZeroOwner.Memory.Span[..tileBufferBytes]; - tileZero.Clear(); - fixed (byte* tilePtr = tileZero) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileBuffer, - 0, - tilePtr, - (nuint)tileBufferBytes); - } - } - - int tileCountsBytes = checked(tileCount * sizeof(uint)); - BufferDescriptor tileCountsDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)tileCountsBytes - }; - - WgpuBuffer* tileCountsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in tileCountsDescriptor); - if (tileCountsBuffer is null) - { - error = "Failed to create tile counts buffer."; - return false; - } - - flushContext.TrackBuffer(tileCountsBuffer); - using (IMemoryOwner tileCountsZeroOwner = configuration.MemoryAllocator.Allocate(tileCountsBytes)) - { - Span tileCountsZero = tileCountsZeroOwner.Memory.Span[..tileCountsBytes]; - tileCountsZero.Clear(); - fixed (byte* tileCountsPtr = tileCountsZero) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - tileCountsBuffer, - 0, - tileCountsPtr, - (nuint)tileCountsBytes); - } - } - - int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); - BufferDescriptor segCountsDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)segCountsBytes - }; + return true; + } - WgpuBuffer* segCountsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segCountsDescriptor); - if (segCountsBuffer is null) + int lineBufferBytes = checked(totalLineCount * LineStrideBytes); + using IMemoryOwner lineUploadOwner = configuration.MemoryAllocator.Allocate(lineBufferBytes); + Span lineUpload = lineUploadOwner.Memory.Span[..lineBufferBytes]; + int mergedLineIndex = 0; + for (int pathIndex = 0; pathIndex < pathBuilds.Length; pathIndex++) + { + CoveragePathBuild build = pathBuilds[pathIndex]; + CachedCoverageGeometry geometry = build.Geometry; + if (geometry.LineCount == 0 || geometry.LineOwner is null) { - error = "Failed to create segment counts buffer."; - return false; + continue; } - flushContext.TrackBuffer(segCountsBuffer); - - int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); - BufferDescriptor segmentsDescriptor = new() + ReadOnlySpan sourceLines = geometry.LineOwner.Memory.Span[..(geometry.LineCount * LineStrideBytes)]; + for (int lineIndex = 0; lineIndex < geometry.LineCount; lineIndex++) { - Usage = BufferUsage.Storage, - Size = (nuint)segmentsBytes - }; - - WgpuBuffer* segmentsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segmentsDescriptor); - if (segmentsBuffer is null) - { - error = "Failed to create segments buffer."; - return false; + int sourceOffset = lineIndex * LineStrideBytes; + float x0 = ReadFloat(sourceLines, sourceOffset + 8) + build.OriginX; + float y0 = ReadFloat(sourceLines, sourceOffset + 12) + build.OriginY; + float x1 = ReadFloat(sourceLines, sourceOffset + 16) + build.OriginX; + float y1 = ReadFloat(sourceLines, sourceOffset + 20) + build.OriginY; + WriteLine(lineUpload, mergedLineIndex, (uint)pathIndex, x0, y0, x1, y1); + mergedLineIndex++; } + } - flushContext.TrackBuffer(segmentsBuffer); - - RasterConfig config = new() - { - WidthInTiles = (uint)widthInTiles, - HeightInTiles = (uint)heightInTiles, - TargetWidth = (uint)interest.Width, - TargetHeight = (uint)interest.Height, - BaseColor = 0, - NDrawObj = 0, - NPath = 1, - NClip = 0, - BinDataStart = 0, - PathtagBase = 0, - PathdataBase = 0, - DrawtagBase = 0, - DrawdataBase = 0, - TransformBase = 0, - StyleBase = 0, - LinesSize = (uint)lineCount, - BinningSize = 1, - TilesSize = (uint)tileCount, - SegCountsSize = segCountsCapacity, - SegmentsSize = segmentsCapacity, - BlendSize = 1, - PtclSize = 1 - }; - - BufferDescriptor configDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; - - WgpuBuffer* configBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in configDescriptor); - if (configBuffer is null) - { - error = "Failed to create config buffer."; - return false; - } + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-lines", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)lineBufferBytes, + out WgpuBuffer* lineBuffer, + out error)) + { + return false; + } - flushContext.TrackBuffer(configBuffer); + fixed (byte* lineUploadPtr = lineUpload) + { flushContext.Api.QueueWriteBuffer( flushContext.Queue, - configBuffer, + lineBuffer, 0, - &config, - (nuint)Unsafe.SizeOf()); - - BumpAllocatorsData bumpData = new() - { - Failed = 0, - Binning = 0, - Ptcl = 0, - Tile = 0, - SegCounts = 0, - Segments = 0, - Blend = 0, - Lines = (uint)lineCount - }; + lineUploadPtr, + (nuint)lineBufferBytes); + } - BufferDescriptor bumpDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; + int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); + using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); + Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; + int tileBase = 0; + for (int i = 0; i < pathBuilds.Length; i++) + { + CoveragePathBuild build = pathBuilds[i]; + WritePath( + pathUpload.Slice(i * PathStrideBytes, PathStrideBytes), + (uint)build.OriginTileX, + (uint)build.OriginTileY, + (uint)(build.OriginTileX + atlasWidthInTiles), + (uint)(build.OriginTileY + build.Geometry.HeightInTiles), + (uint)tileBase); + tileBase = checked(tileBase + (atlasWidthInTiles * build.Geometry.HeightInTiles)); + } - WgpuBuffer* bumpBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in bumpDescriptor); - if (bumpBuffer is null) - { - error = "Failed to create bump buffer."; - return false; - } + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-paths", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)pathBufferBytes, + out WgpuBuffer* pathBuffer, + out error)) + { + return false; + } - flushContext.TrackBuffer(bumpBuffer); + fixed (byte* pathUploadPtr = pathUpload) + { flushContext.Api.QueueWriteBuffer( flushContext.Queue, - bumpBuffer, + pathBuffer, 0, - &bumpData, - (nuint)Unsafe.SizeOf()); + pathUploadPtr, + (nuint)pathBufferBytes); + } - IndirectCountData indirectData = default; - BufferDescriptor indirectDescriptor = new() - { - Usage = BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; + int tileBufferBytes = checked(totalTileCount * TileStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-tiles", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileBufferBytes, + out WgpuBuffer* tileBuffer, + out error)) + { + return false; + } - WgpuBuffer* indirectBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in indirectDescriptor); - if (indirectBuffer is null) - { - error = "Failed to create indirect dispatch buffer."; - return false; - } + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileBuffer, + 0, + (nuint)tileBufferBytes); + + int tileCountsBytes = checked(totalTileCount * sizeof(uint)); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-tile-counts", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileCountsBytes, + out WgpuBuffer* tileCountsBuffer, + out error)) + { + return false; + } - flushContext.TrackBuffer(indirectBuffer); - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - indirectBuffer, - 0, - &indirectData, - (nuint)Unsafe.SizeOf()); + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileCountsBuffer, + 0, + (nuint)tileCountsBytes); - SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)tileCount }; - BufferDescriptor segmentAllocDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; + if (totalEstimatedSegments > int.MaxValue) + { + error = "Coverage segment estimate overflow."; + return false; + } - WgpuBuffer* segmentAllocBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in segmentAllocDescriptor); - if (segmentAllocBuffer is null) - { - error = "Failed to create segment allocation buffer."; - return false; - } + uint segCountsCapacity = totalEstimatedSegments == 0 ? 1u : checked((uint)totalEstimatedSegments); + uint segmentsCapacity = segCountsCapacity; + int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segment-counts", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)segCountsBytes, + out WgpuBuffer* segCountsBuffer, + out error)) + { + return false; + } - flushContext.TrackBuffer(segmentAllocBuffer); - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - segmentAllocBuffer, - 0, - &segmentAllocConfig, - (nuint)Unsafe.SizeOf()); + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + segCountsBuffer, + 0, + (nuint)segCountsBytes); + + int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segments", + BufferUsage.Storage, + (nuint)segmentsBytes, + out WgpuBuffer* segmentsBuffer, + out error)) + { + return false; + } - uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; - uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; - CoverageConfig coverageConfig = new() - { - TargetWidth = (uint)interest.Width, - TargetHeight = (uint)interest.Height, - TileOriginX = (uint)tileMinX, - TileOriginY = (uint)tileMinY, - TileWidthInTiles = (uint)tileWidth, - TileHeightInTiles = (uint)tileHeight, - FillRule = fillRule, - IsAliased = isAliased - }; + RasterConfig config = new() + { + WidthInTiles = (uint)atlasWidthInTiles, + HeightInTiles = (uint)atlasHeightInTiles, + TargetWidth = (uint)atlasWidth, + TargetHeight = (uint)atlasHeight, + BaseColor = 0, + NDrawObj = 0, + NPath = (uint)pathBuilds.Length, + NClip = 0, + BinDataStart = 0, + PathtagBase = 0, + PathdataBase = 0, + DrawtagBase = 0, + DrawdataBase = 0, + TransformBase = 0, + StyleBase = 0, + LinesSize = (uint)totalLineCount, + BinningSize = (uint)pathBuilds.Length, + TilesSize = (uint)totalTileCount, + SegCountsSize = segCountsCapacity, + SegmentsSize = segmentsCapacity, + BlendSize = 1, + PtclSize = 1 + }; - BufferDescriptor coverageConfigDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-raster-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* configBuffer, + out error)) + { + return false; + } - WgpuBuffer* coverageConfigBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in coverageConfigDescriptor); - if (coverageConfigBuffer is null) - { - error = "Failed to create coverage config buffer."; - return false; - } + flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, (nuint)Unsafe.SizeOf()); - flushContext.TrackBuffer(coverageConfigBuffer); - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - coverageConfigBuffer, - 0, - &coverageConfig, - (nuint)Unsafe.SizeOf()); + BumpAllocatorsData bumpData = new() + { + Failed = 0, + Binning = 0, + Ptcl = 0, + Tile = 0, + SegCounts = 0, + Segments = 0, + Blend = 0, + Lines = (uint)totalLineCount + }; - if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error)) - { - return false; - } + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-bump", + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* bumpBuffer, + out error)) + { + return false; + } - if (!this.DispatchPathCount( - flushContext, - configBuffer, - bumpBuffer, - lineBuffer, - pathBuffer, - tileBuffer, - segCountsBuffer, - indirectBuffer, - out error)) - { - return false; - } + flushContext.Api.QueueWriteBuffer(flushContext.Queue, bumpBuffer, 0, &bumpData, (nuint)Unsafe.SizeOf()); - if (!this.DispatchBackdrop( - flushContext, - configBuffer, - tileBuffer, - heightInTiles, - out error)) - { - return false; - } + IndirectCountData indirectData = default; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-indirect", + BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* indirectBuffer, + out error)) + { + return false; + } - if (!this.DispatchSegmentAlloc( - flushContext, - bumpBuffer, - tileBuffer, - tileCountsBuffer, - segmentAllocBuffer, - tileCount, - out error)) - { - return false; - } + flushContext.Api.QueueWriteBuffer(flushContext.Queue, indirectBuffer, 0, &indirectData, (nuint)Unsafe.SizeOf()); - if (!this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error)) - { - return false; - } + SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)totalTileCount }; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-segment-alloc", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* segmentAllocBuffer, + out error)) + { + return false; + } - if (!this.DispatchPathTiling( - flushContext, - bumpBuffer, - segCountsBuffer, - lineBuffer, - pathBuffer, - tileBuffer, - segmentsBuffer, - indirectBuffer, - out error)) - { - return false; - } + flushContext.Api.QueueWriteBuffer(flushContext.Queue, segmentAllocBuffer, 0, &segmentAllocConfig, (nuint)Unsafe.SizeOf()); - if (!this.DispatchCoverageFine( - flushContext, - coverageConfigBuffer, - tileBuffer, - tileCountsBuffer, - segmentsBuffer, - coverageView, - tileWidth, - tileHeight, - out error)) - { - return false; - } + CoverageConfig coverageConfig = new() + { + TargetWidth = (uint)atlasWidth, + TargetHeight = (uint)atlasHeight, + TileOriginX = 0, + TileOriginY = 0, + TileWidthInTiles = (uint)atlasWidthInTiles, + TileHeightInTiles = (uint)atlasHeightInTiles, + FillRule = fillRuleValue.GetValueOrDefault(0), + IsAliased = aliasedValue.GetValueOrDefault(0) + }; - error = null; - return true; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-coverage-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* coverageConfigBuffer, + out error)) + { + return false; } - finally + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, coverageConfigBuffer, 0, &coverageConfig, (nuint)Unsafe.SizeOf()); + + if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error) || + !this.DispatchPathCount(flushContext, configBuffer, bumpBuffer, lineBuffer, pathBuffer, tileBuffer, segCountsBuffer, indirectBuffer, out error) || + !this.DispatchBackdrop(flushContext, configBuffer, tileBuffer, atlasHeightInTiles, out error) || + !this.DispatchSegmentAlloc(flushContext, bumpBuffer, tileBuffer, tileCountsBuffer, segmentAllocBuffer, totalTileCount, out error) || + !this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error) || + !this.DispatchPathTiling(flushContext, bumpBuffer, segCountsBuffer, lineBuffer, pathBuffer, tileBuffer, segmentsBuffer, indirectBuffer, out error) || + !this.DispatchCoverageFine(flushContext, coverageConfigBuffer, tileBuffer, tileCountsBuffer, segmentsBuffer, coverageView, atlasWidthInTiles, atlasHeightInTiles, out error)) { - lineOwner?.Dispose(); + return false; } + + error = null; + return true; } private static bool TryGetOrCreateCoverageBuffer( @@ -936,6 +482,16 @@ private static bool TryGetOrCreateCoverageBuffer( out _, out error); + private void DisposeCoverageResources() + { + foreach (CachedCoverageGeometry geometry in this.coverageGeometryCache.Values) + { + geometry.Dispose(); + } + + this.coverageGeometryCache.Clear(); + } + private static bool TryBuildLineBuffer( IPath path, in Rectangle interest, @@ -1940,40 +1496,20 @@ private static bool TryCreateCoverageFineBindGroupLayout( private readonly struct CoveragePathBuild { public CoveragePathBuild( - IMemoryOwner? lineOwner, - int lineCount, - uint estimatedSegments, - int widthInTiles, - int heightInTiles, + CachedCoverageGeometry geometry, int originTileX, int originTileY, int originX, - int originY, - int coverageWidth, - int coverageHeight) + int originY) { - this.LineOwner = lineOwner; - this.LineCount = lineCount; - this.EstimatedSegments = estimatedSegments; - this.WidthInTiles = widthInTiles; - this.HeightInTiles = heightInTiles; + this.Geometry = geometry; this.OriginTileX = originTileX; this.OriginTileY = originTileY; this.OriginX = originX; this.OriginY = originY; - this.CoverageWidth = coverageWidth; - this.CoverageHeight = coverageHeight; } - public IMemoryOwner? LineOwner { get; } - - public int LineCount { get; } - - public uint EstimatedSegments { get; } - - public int WidthInTiles { get; } - - public int HeightInTiles { get; } + public CachedCoverageGeometry Geometry { get; } public int OriginTileX { get; } @@ -1982,10 +1518,6 @@ public CoveragePathBuild( public int OriginX { get; } public int OriginY { get; } - - public int CoverageWidth { get; } - - public int CoverageHeight { get; } } [StructLayout(LayoutKind.Sequential)] @@ -2059,4 +1591,42 @@ private struct CoverageConfig public uint FillRule; public uint IsAliased; } + + private sealed class CachedCoverageGeometry : IDisposable + { + public CachedCoverageGeometry( + IMemoryOwner? lineOwner, + int lineCount, + uint estimatedSegments, + int widthInTiles, + int heightInTiles, + int coverageWidth, + int coverageHeight) + { + this.LineOwner = lineOwner; + this.LineCount = lineCount; + this.EstimatedSegments = estimatedSegments; + this.WidthInTiles = widthInTiles; + this.HeightInTiles = heightInTiles; + this.CoverageWidth = coverageWidth; + this.CoverageHeight = coverageHeight; + } + + public IMemoryOwner? LineOwner { get; } + + public int LineCount { get; } + + public uint EstimatedSegments { get; } + + public int WidthInTiles { get; } + + public int HeightInTiles { get; } + + public int CoverageWidth { get; } + + public int CoverageHeight { get; } + + public void Dispose() + => this.LineOwner?.Dispose(); + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index c7b00e7b4..013d1caf0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -40,18 +40,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { - private const uint CompositeVertexCount = 6; private const int CompositeComputeWorkgroupSize = 8; private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; private const int CompositeTileCommandWorkgroupSize = 64; - private const int CompositeDestinationPixelStride = 16; private const uint PreparedBrushTypeSolid = 0; private const uint PreparedBrushTypeImage = 1; private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; private const string PreparedCompositeTileCountsBufferKey = "prepared-composite/tile-counts"; private const string PreparedCompositeTileStartsBufferKey = "prepared-composite/tile-starts"; - private const string PreparedCompositeTileWriteOffsetsBufferKey = "prepared-composite/tile-write-offsets"; + private const string PreparedCompositeTileOffsetsBufferKey = "prepared-composite/tile-offsets"; private const string PreparedCompositeTileIndicesBufferKey = "prepared-composite/tile-indices"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; private const int CallbackTimeoutMilliseconds = 10_000; @@ -207,6 +205,8 @@ public void FlushCompositions( return; } + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( compositionScene.Commands, target.Bounds); @@ -262,7 +262,6 @@ public void FlushCompositions( bool gpuSuccess = false; bool gpuReady = false; string? failure = null; - TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); int pixelSizeInBytes = Unsafe.SizeOf(); using WebGPUFlushContext flushContext = WebGPUFlushContext.Create( target, @@ -430,70 +429,53 @@ private bool TryRenderPreparedFlush( return false; } - WgpuBuffer* destinationPixelsBuffer = flushContext.CompositeDestinationPixelsBuffer; - nuint destinationPixelsByteSize = flushContext.CompositeDestinationPixelsByteSize; - bool hasValidDestinationBuffer = - destinationPixelsBuffer is not null && - destinationPixelsByteSize != 0 && - flushContext.CompositeDestinationWidth == targetLocalBounds.Width && - flushContext.CompositeDestinationHeight == targetLocalBounds.Height; - - TextureView* sourceTextureView = flushContext.TargetView; + TextureView* backdropTextureView = flushContext.TargetView; int sourceOriginX = targetLocalBounds.X; int sourceOriginY = targetLocalBounds.Y; - if (!flushContext.CanSampleTargetTexture) + Texture* outputTexture = flushContext.TargetTexture; + TextureView* outputTextureView = flushContext.TargetView; + bool writesDirectlyToTarget = !flushContext.RequiresReadback; + int outputOriginX = writesDirectlyToTarget ? targetLocalBounds.X : 0; + int outputOriginY = writesDirectlyToTarget ? targetLocalBounds.Y : 0; + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out Texture* sourceTexture, + out backdropTextureView, + out error)) { - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out Texture* sourceTexture, - out sourceTextureView, - out error)) - { - return false; - } - - CopyTextureRegion(flushContext, flushContext.TargetTexture, sourceTexture, targetLocalBounds); - sourceOriginX = 0; - sourceOriginY = 0; + return false; } - if (!hasValidDestinationBuffer) - { - if (!TryCreateDestinationPixelsBuffer( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out destinationPixelsBuffer, - out destinationPixelsByteSize, - out error)) - { - return false; - } - - flushContext.CompositeDestinationPixelsBuffer = destinationPixelsBuffer; - flushContext.CompositeDestinationPixelsByteSize = destinationPixelsByteSize; - flushContext.CompositeDestinationWidth = targetLocalBounds.Width; - flushContext.CompositeDestinationHeight = targetLocalBounds.Height; - } + CopyTextureRegion( + flushContext, + flushContext.TargetTexture, + targetLocalBounds.X, + targetLocalBounds.Y, + sourceTexture, + 0, + 0, + targetLocalBounds.Width, + targetLocalBounds.Height); + sourceOriginX = 0; + sourceOriginY = 0; - if (!TryInitializeDestinationPixels( + if (!writesDirectlyToTarget && + !TryCreateCompositionTexture( flushContext, - sourceTextureView, - destinationPixelsBuffer, - targetLocalBounds, - sourceOriginX, - sourceOriginY, - destinationPixelsByteSize, + targetLocalBounds.Width, + targetLocalBounds.Height, + out outputTexture, + out outputTextureView, out error)) { return false; } List coverageDefinitions = new(); - Dictionary coverageDefinitionIndexByKey = new(); - List pendingCommands = new(); + Dictionary coverageDefinitionIndexByKey = new(); + List flushCommands = new(); for (int i = 0; i < preparedBatches.Count; i++) { CompositionBatch batch = preparedBatches[i]; @@ -516,18 +498,21 @@ destinationPixelsBuffer is not null && if (!sawVisibleCommand) { - int definitionKey = batch.Definition.DefinitionKey; - if (!coverageDefinitionIndexByKey.TryGetValue(definitionKey, out coverageDefinitionIndex)) + CoverageDefinitionIdentity definitionIdentity = new(batch.Definition); + if (!coverageDefinitionIndexByKey.TryGetValue(definitionIdentity, out coverageDefinitionIndex)) { coverageDefinitionIndex = coverageDefinitions.Count; coverageDefinitions.Add(batch.Definition); - coverageDefinitionIndexByKey.Add(definitionKey, coverageDefinitionIndex); + coverageDefinitionIndexByKey.Add(definitionIdentity, coverageDefinitionIndex); } sawVisibleCommand = true; } - pendingCommands.Add(new PreparedCompositePendingCommand(coverageDefinitionIndex, command)); + Point clippedSourceOffset = new( + command.SourceOffset.X + (clippedDestination.X - command.DestinationRegion.X), + command.SourceOffset.Y + (clippedDestination.Y - command.DestinationRegion.Y)); + flushCommands.Add(new FlushCompositeCommand(coverageDefinitionIndex, command, clippedDestination, clippedSourceOffset)); } if (sawVisibleCommand) @@ -536,7 +521,7 @@ destinationPixelsBuffer is not null && } } - if (pendingCommands.Count == 0) + if (flushCommands.Count == 0) { error = null; return true; @@ -555,12 +540,15 @@ destinationPixelsBuffer is not null && if (!this.TryDispatchPreparedCompositeCommands( flushContext, - sourceTextureView, - destinationPixelsBuffer, - destinationPixelsByteSize, + backdropTextureView, + outputTextureView, targetBounds, targetLocalBounds, - pendingCommands, + sourceOriginX, + sourceOriginY, + outputOriginX, + outputOriginY, + flushCommands, coveragePlacements, coverageView, out error)) @@ -568,14 +556,18 @@ destinationPixelsBuffer is not null && return false; } - if (!TryBlitDestinationPixelsToTarget( - flushContext, - destinationPixelsBuffer, - destinationPixelsByteSize, - targetLocalBounds, - out error)) + if (!writesDirectlyToTarget) { - return false; + CopyTextureRegion( + flushContext, + outputTexture, + 0, + 0, + flushContext.TargetTexture, + targetLocalBounds.X, + targetLocalBounds.Y, + targetLocalBounds.Width, + targetLocalBounds.Height); } error = null; @@ -584,12 +576,15 @@ destinationPixelsBuffer is not null && private bool TryDispatchPreparedCompositeCommands( WebGPUFlushContext flushContext, - TextureView* defaultBrushTextureView, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, + TextureView* backdropTextureView, + TextureView* outputTextureView, Rectangle targetBounds, Rectangle targetLocalBounds, - IReadOnlyList flushCommands, + int sourceOriginX, + int sourceOriginY, + int outputOriginX, + int outputOriginY, + IReadOnlyList flushCommands, CoveragePlacement[] coveragePlacements, TextureView* coverageTextureView, out string? error) @@ -601,10 +596,31 @@ private bool TryDispatchPreparedCompositeCommands( return true; } + if (!PreparedCompositeFineComputeShader.TryGetCode(flushContext.TextureFormat, out byte[] shaderCode, out error)) + { + return false; + } + + if (!PreparedCompositeFineComputeShader.TryGetInputSampleType(flushContext.TextureFormat, out TextureSampleType inputTextureSampleType)) + { + error = $"Prepared composite fine shader does not support texture format '{flushContext.TextureFormat}'."; + return false; + } + + string pipelineKey = $"prepared-composite-fine/{flushContext.TextureFormat}"; + WebGPUCompositeBindGroupLayoutFactory layoutFactory = (WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + => TryCreatePreparedCompositeFineBindGroupLayout( + api, + device, + flushContext.TextureFormat, + inputTextureSampleType, + out layout, + out layoutError); + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - "prepared-composite", - PreparedCompositeComputeShader.Code, - TryCreatePreparedCompositeBindGroupLayout, + pipelineKey, + shaderCode, + layoutFactory, out BindGroupLayout* bindGroupLayout, out ComputePipeline* pipeline, out error)) @@ -625,20 +641,17 @@ private bool TryDispatchPreparedCompositeCommands( flushContext.MemoryAllocator.Allocate(flushCommands.Count); try { + int flushCommandCount = flushCommands.Count; Span parameters = parametersOwner.Memory.Span[..flushCommands.Count]; - TextureView* sourceTextureView = defaultBrushTextureView; - nint sourceTextureViewHandle = (nint)defaultBrushTextureView; + TextureView* brushTextureView = backdropTextureView; + nint brushTextureViewHandle = (nint)backdropTextureView; bool hasImageTexture = false; - uint validCommandCount = 0; + uint totalTilePairCount = 0; - for (int i = 0; i < flushCommands.Count; i++) + for (int i = 0; i < flushCommandCount; i++) { - PreparedCompositePendingCommand pendingCommand = flushCommands[i]; - PreparedCompositionCommand command = pendingCommand.Command; - if (command.DestinationRegion.Width <= 0 || command.DestinationRegion.Height <= 0) - { - continue; - } + FlushCompositeCommand flushCommand = flushCommands[i]; + PreparedCompositionCommand command = flushCommand.Command; uint brushType; int brushOriginX = 0; @@ -663,7 +676,7 @@ private bool TryDispatchPreparedCompositeCommands( flushContext, image, flushContext.TextureFormat, - out TextureView* brushTextureView, + out TextureView* resolvedBrushTextureView, out error)) { return false; @@ -671,11 +684,11 @@ private bool TryDispatchPreparedCompositeCommands( if (!hasImageTexture) { - sourceTextureView = brushTextureView; - sourceTextureViewHandle = (nint)brushTextureView; + brushTextureView = resolvedBrushTextureView; + brushTextureViewHandle = (nint)resolvedBrushTextureView; hasImageTexture = true; } - else if (sourceTextureViewHandle != (nint)brushTextureView) + else if (brushTextureViewHandle != (nint)resolvedBrushTextureView) { error = "Prepared composite flush currently supports one image brush texture per dispatch."; return false; @@ -695,17 +708,26 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - CoveragePlacement coveragePlacement = coveragePlacements[pendingCommand.CoverageDefinitionIndex]; - - int destinationX = command.DestinationRegion.X - targetLocalBounds.X; - int destinationY = command.DestinationRegion.Y - targetLocalBounds.Y; + CoveragePlacement coveragePlacement = coveragePlacements[flushCommand.CoverageDefinitionIndex]; + Rectangle destinationRegion = flushCommand.DestinationRegion; + Point sourceOffset = flushCommand.SourceOffset; + + int destinationX = destinationRegion.X - targetLocalBounds.X; + int destinationY = destinationRegion.Y - targetLocalBounds.Y; + int minTileX = destinationX / CompositeTileWidth; + int minTileY = destinationY / CompositeTileHeight; + int maxTileX = (destinationX + destinationRegion.Width - 1) / CompositeTileWidth; + int maxTileY = (destinationY + destinationRegion.Height - 1) / CompositeTileHeight; + uint tileEmitCount = checked((uint)((maxTileX - minTileX + 1) * (maxTileY - minTileY + 1))); + uint tileEmitOffset = totalTilePairCount; + totalTilePairCount = checked(totalTilePairCount + tileEmitCount); PreparedCompositeParameters commandParameters = new( destinationX, destinationY, - command.DestinationRegion.Width, - command.DestinationRegion.Height, - command.SourceOffset.X + coveragePlacement.OriginX, - command.SourceOffset.Y + coveragePlacement.OriginY, + destinationRegion.Width, + destinationRegion.Height, + sourceOffset.X + coveragePlacement.OriginX, + sourceOffset.Y + coveragePlacement.OriginY, targetLocalBounds.Width, brushType, brushOriginX, @@ -717,21 +739,14 @@ private bool TryDispatchPreparedCompositeCommands( (uint)command.GraphicsOptions.ColorBlendingMode, (uint)command.GraphicsOptions.AlphaCompositionMode, command.GraphicsOptions.BlendPercentage, - solidColor); - - uint parameterIndex = validCommandCount; - parameters[(int)parameterIndex] = commandParameters; - - validCommandCount++; - } + solidColor, + tileEmitOffset, + tileEmitCount); - if (validCommandCount == 0) - { - error = null; - return true; + parameters[i] = commandParameters; } - int usedParameterByteCount = checked((int)(validCommandCount * parameterSize)); + int usedParameterByteCount = checked(flushCommandCount * (int)parameterSize); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeParamsBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, @@ -743,8 +758,7 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - Span usedParameters = parameters[..(int)validCommandCount]; - fixed (PreparedCompositeParameters* usedParametersPtr = usedParameters) + fixed (PreparedCompositeParameters* usedParametersPtr = parameters) { flushContext.Api.QueueWriteBuffer( flushContext.Queue, @@ -775,7 +789,7 @@ private bool TryDispatchPreparedCompositeCommands( int tileStartsByteCount = checked(tileCount * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeTileStartsBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, + BufferUsage.Storage | BufferUsage.CopyDst | BufferUsage.CopySrc, (nuint)tileStartsByteCount, out WgpuBuffer* tileStartsBuffer, out _, @@ -784,25 +798,13 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileWriteOffsetsBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileStartsByteCount, - out WgpuBuffer* tileWriteOffsetsBuffer, - out _, - out error)) - { - return false; - } - - nuint maxTileCommandIndices = checked((nuint)validCommandCount * (nuint)tileCount); - if (maxTileCommandIndices == 0) + if (totalTilePairCount == 0) { error = null; return true; } - nuint tileCommandIndicesByteCount = checked(maxTileCommandIndices * (nuint)sizeof(uint)); + nuint tileCommandIndicesByteCount = checked((nuint)totalTilePairCount * (nuint)sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeTileIndicesBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, @@ -814,6 +816,17 @@ private bool TryDispatchPreparedCompositeCommands( return false; } + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeTileOffsetsBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)tileStartsByteCount, + out WgpuBuffer* tileOffsetsBuffer, + out _, + out error)) + { + return false; + } + nuint dispatchConfigSize = (nuint)Unsafe.SizeOf(); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeDispatchConfigBufferKey, @@ -832,7 +845,11 @@ private bool TryDispatchPreparedCompositeCommands( (uint)tileCountX, (uint)tileCountY, (uint)tileCount, - validCommandCount); + (uint)flushCommandCount, + (uint)sourceOriginX, + (uint)sourceOriginY, + (uint)outputOriginX, + (uint)outputOriginY); flushContext.Api.QueueWriteBuffer( flushContext.Queue, dispatchConfigBuffer, @@ -845,7 +862,7 @@ private bool TryDispatchPreparedCompositeCommands( paramsBuffer, tileCountsBuffer, dispatchConfigBuffer, - validCommandCount, + (uint)flushCommandCount, out error)) { return false; @@ -861,26 +878,39 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - flushContext.Api.CommandEncoderClearBuffer( + flushContext.Api.CommandEncoderCopyBufferToBuffer( flushContext.CommandEncoder, - tileWriteOffsetsBuffer, + tileStartsBuffer, + 0, + tileOffsetsBuffer, 0, (nuint)tileStartsByteCount); if (!this.DispatchPreparedCompositeTileScatter( flushContext, paramsBuffer, + tileOffsetsBuffer, + tileCommandIndicesBuffer, + dispatchConfigBuffer, + (uint)flushCommandCount, + out error)) + { + return false; + } + + if (!this.DispatchPreparedCompositeTileSort( + flushContext, tileStartsBuffer, - tileWriteOffsetsBuffer, + tileCountsBuffer, tileCommandIndicesBuffer, dispatchConfigBuffer, - validCommandCount, + (uint)tileCount, out error)) { return false; } - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[9]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -889,46 +919,49 @@ private bool TryDispatchPreparedCompositeCommands( bindGroupEntries[1] = new BindGroupEntry { Binding = 1, - TextureView = sourceTextureView + TextureView = backdropTextureView }; bindGroupEntries[2] = new BindGroupEntry { Binding = 2, - Buffer = destinationPixelsBuffer, - Offset = 0, - Size = destinationPixelsByteSize + TextureView = brushTextureView }; bindGroupEntries[3] = new BindGroupEntry { Binding = 3, + TextureView = outputTextureView + }; + bindGroupEntries[4] = new BindGroupEntry + { + Binding = 4, Buffer = paramsBuffer, Offset = 0, Size = (nuint)usedParameterByteCount }; - bindGroupEntries[4] = new BindGroupEntry + bindGroupEntries[5] = new BindGroupEntry { - Binding = 4, + Binding = 5, Buffer = tileStartsBuffer, Offset = 0, Size = (nuint)tileStartsByteCount }; - bindGroupEntries[5] = new BindGroupEntry + bindGroupEntries[6] = new BindGroupEntry { - Binding = 5, + Binding = 6, Buffer = tileCountsBuffer, Offset = 0, Size = (nuint)tileCountsByteCount }; - bindGroupEntries[6] = new BindGroupEntry + bindGroupEntries[7] = new BindGroupEntry { - Binding = 6, + Binding = 7, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = tileCommandIndicesByteCount }; - bindGroupEntries[7] = new BindGroupEntry + bindGroupEntries[8] = new BindGroupEntry { - Binding = 7, + Binding = 8, Buffer = dispatchConfigBuffer, Offset = 0, Size = dispatchConfigSize @@ -937,7 +970,7 @@ private bool TryDispatchPreparedCompositeCommands( BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 8, + EntryCount = 9, Entries = bindGroupEntries }; @@ -963,9 +996,9 @@ private bool TryDispatchPreparedCompositeCommands( flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); flushContext.Api.ComputePassEncoderDispatchWorkgroups( passEncoder, - DivideRoundUp(targetLocalBounds.Width, CompositeComputeWorkgroupSize), - DivideRoundUp(targetLocalBounds.Height, CompositeComputeWorkgroupSize), - 1); + DivideRoundUp(CompositeTileWidth, CompositeComputeWorkgroupSize), + DivideRoundUp(CompositeTileHeight, CompositeComputeWorkgroupSize), + (uint)tileCount); } finally { @@ -1026,14 +1059,17 @@ private bool DispatchPreparedCompositeTilePrefix( entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; return 3; }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( + pass, + 1, + 1, + 1), out error); private bool DispatchPreparedCompositeTileScatter( WebGPUFlushContext flushContext, WgpuBuffer* paramsBuffer, - WgpuBuffer* tileStartsBuffer, - WgpuBuffer* tileWriteOffsetsBuffer, + WgpuBuffer* tileOffsetsBuffer, WgpuBuffer* tileCommandIndicesBuffer, WgpuBuffer* dispatchConfigBuffer, uint commandCount, @@ -1046,11 +1082,10 @@ private bool DispatchPreparedCompositeTileScatter( (entries) => { entries[0] = new BindGroupEntry { Binding = 0, Buffer = paramsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileWriteOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 5; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 4; }, (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( pass, @@ -1059,6 +1094,34 @@ private bool DispatchPreparedCompositeTileScatter( 1), out error); + private bool DispatchPreparedCompositeTileSort( + WebGPUFlushContext flushContext, + WgpuBuffer* tileStartsBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* tileCommandIndicesBuffer, + WgpuBuffer* dispatchConfigBuffer, + uint tileCount, + out string? error) + => this.DispatchComputePass( + flushContext, + "prepared-composite-tile-sort", + PreparedCompositeTileSortComputeShader.Code, + TryCreatePreparedCompositeTileSortBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 4; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( + pass, + tileCount, + 1, + 1), + out error); + private static bool TryGetOrCreateImageTextureView( WebGPUFlushContext flushContext, Image image, @@ -1126,376 +1189,18 @@ private static bool TryGetOrCreateImageTextureView( return true; } - /// - /// Allocates destination storage used by compute composition. - /// - private static bool TryCreateDestinationPixelsBuffer( - WebGPUFlushContext flushContext, - int width, - int height, - out WgpuBuffer* destinationPixelsBuffer, - out nuint destinationPixelsByteSize, - out string? error) - { - destinationPixelsByteSize = checked((nuint)width * (nuint)height * CompositeDestinationPixelStride); - BufferDescriptor descriptor = new() - { - Usage = BufferUsage.Storage, - Size = destinationPixelsByteSize - }; - - destinationPixelsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); - if (destinationPixelsBuffer is null) - { - error = "Failed to create destination pixel storage buffer."; - return false; - } - - flushContext.TrackBuffer(destinationPixelsBuffer); - error = null; - return true; - } - - /// - /// Initializes destination storage from the current destination texture contents in premultiplied form. - /// - private static bool TryInitializeDestinationPixels( - WebGPUFlushContext flushContext, - TextureView* sourceTextureView, - WgpuBuffer* destinationPixelsBuffer, - in Rectangle destinationBounds, - int sourceOriginX, - int sourceOriginY, - nuint destinationPixelsByteSize, - out string? error) - { - if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - "composite-destination-init", - CompositeDestinationInitShader.Code, - TryCreateDestinationInitBindGroupLayout, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, - out error)) - { - return false; - } - - BufferDescriptor paramsDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = (nuint)Unsafe.SizeOf() - }; - - WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); - if (paramsBuffer is null) - { - error = "Failed to create destination initialization parameter buffer."; - return false; - } - - flushContext.TrackBuffer(paramsBuffer); - CompositeDestinationInitParameters parameters = new( - destinationBounds.Width, - destinationBounds.Height, - sourceOriginX, - sourceOriginY); - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - paramsBuffer, - 0, - ¶meters, - (nuint)Unsafe.SizeOf()); - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[3]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - TextureView = sourceTextureView - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = destinationPixelsBuffer, - Offset = 0, - Size = destinationPixelsByteSize - }; - bindGroupEntries[2] = new BindGroupEntry - { - Binding = 2, - Buffer = paramsBuffer, - Offset = 0, - Size = (nuint)Unsafe.SizeOf() - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = bindGroupLayout, - EntryCount = 3, - Entries = bindGroupEntries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - error = "Failed to create destination initialization bind group."; - return false; - } - - flushContext.TrackBindGroup(bindGroup); - ComputePassDescriptor passDescriptor = default; - ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); - if (passEncoder is null) - { - error = "Failed to begin destination initialization compute pass."; - return false; - } - - try - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - uint dispatchX = DivideRoundUp(destinationBounds.Width, CompositeComputeWorkgroupSize); - uint dispatchY = DivideRoundUp(destinationBounds.Height, CompositeComputeWorkgroupSize); - flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, dispatchX, dispatchY, 1); - } - finally - { - flushContext.Api.ComputePassEncoderEnd(passEncoder); - flushContext.Api.ComputePassEncoderRelease(passEncoder); - } - - error = null; - return true; - } - - /// - /// Writes composed premultiplied destination storage back to the render target through a fullscreen blit. - /// - private static bool TryBlitDestinationPixelsToTarget( - WebGPUFlushContext flushContext, - WgpuBuffer* destinationPixelsBuffer, - nuint destinationPixelsByteSize, - in Rectangle destinationBounds, - out string? error) - { - if (!flushContext.DeviceState.TryGetOrCreateCompositePipeline( - "composite-destination-blit", - CompositeDestinationBlitShader.Code, - TryCreateDestinationBlitBindGroupLayout, - flushContext.TextureFormat, - CompositePipelineBlendMode.None, - out BindGroupLayout* bindGroupLayout, - out RenderPipeline* pipeline, - out error)) - { - return false; - } - - BufferDescriptor paramsDescriptor = new() - { - Usage = BufferUsage.Uniform | BufferUsage.CopyDst, - Size = 16 - }; - WgpuBuffer* paramsBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in paramsDescriptor); - if (paramsBuffer is null) - { - error = "Failed to create destination blit parameter buffer."; - return false; - } - - flushContext.TrackBuffer(paramsBuffer); - CompositeDestinationBlitParameters parameters = new( - destinationBounds.Width, - destinationBounds.Height, - destinationBounds.X, - destinationBounds.Y); - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - paramsBuffer, - 0, - ¶meters, - (nuint)Unsafe.SizeOf()); - - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[2]; - bindGroupEntries[0] = new BindGroupEntry - { - Binding = 0, - Buffer = destinationPixelsBuffer, - Offset = 0, - Size = destinationPixelsByteSize - }; - bindGroupEntries[1] = new BindGroupEntry - { - Binding = 1, - Buffer = paramsBuffer, - Offset = 0, - Size = (nuint)Unsafe.SizeOf() - }; - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = bindGroupLayout, - EntryCount = 2, - Entries = bindGroupEntries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - error = "Failed to create destination blit bind group."; - return false; - } - - flushContext.TrackBindGroup(bindGroup); - if (!flushContext.BeginRenderPass(flushContext.TargetView, loadExisting: true)) - { - error = "Failed to begin destination blit render pass."; - return false; - } - - flushContext.Api.RenderPassEncoderSetPipeline(flushContext.PassEncoder, pipeline); - flushContext.Api.RenderPassEncoderSetBindGroup(flushContext.PassEncoder, 0, bindGroup, 0, null); - flushContext.Api.RenderPassEncoderSetViewport( - flushContext.PassEncoder, - 0, - 0, - flushContext.TargetBounds.Width, - flushContext.TargetBounds.Height, - 0, - 1); - flushContext.Api.RenderPassEncoderSetScissorRect( - flushContext.PassEncoder, - (uint)destinationBounds.X, - (uint)destinationBounds.Y, - (uint)destinationBounds.Width, - (uint)destinationBounds.Height); - flushContext.Api.RenderPassEncoderDraw(flushContext.PassEncoder, CompositeVertexCount, 1, 0, 0); - flushContext.EndRenderPassIfOpen(); - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by destination initialization compute shader. - /// - private static bool TryCreateDestinationInitBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 3, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create destination init bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by destination blit render shader. - /// - private static bool TryCreateDestinationBlitBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Fragment, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 2, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create destination blit bind group layout."; - return false; - } - - error = null; - return true; - } - /// /// Creates the bind-group layout used by prepared composite compute shader. /// - private static bool TryCreatePreparedCompositeBindGroupLayout( + private static bool TryCreatePreparedCompositeFineBindGroupLayout( WebGPU api, Device* device, + TextureFormat outputTextureFormat, + TextureSampleType inputTextureSampleType, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1513,7 +1218,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Visibility = ShaderStage.Compute, Texture = new TextureBindingLayout { - SampleType = TextureSampleType.Float, + SampleType = inputTextureSampleType, ViewDimension = TextureViewDimension.Dimension2D, Multisampled = false } @@ -1522,22 +1227,22 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( { Binding = 2, Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + Texture = new TextureBindingLayout { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false } }; entries[3] = new BindGroupLayoutEntry { Binding = 3, Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + StorageTexture = new StorageTextureBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 + Access = StorageTextureAccess.WriteOnly, + Format = outputTextureFormat, + ViewDimension = TextureViewDimension.Dimension2D } }; entries[4] = new BindGroupLayoutEntry @@ -1557,7 +1262,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1568,7 +1273,7 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, + Type = BufferBindingType.Storage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1578,6 +1283,17 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( Binding = 7, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[8] = new BindGroupLayoutEntry + { + Binding = 8, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1587,14 +1303,14 @@ private static bool TryCreatePreparedCompositeBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 8, + EntryCount = 9, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite bind group layout."; + error = "Failed to create prepared composite fine bind group layout."; return false; } @@ -1724,7 +1440,7 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1742,7 +1458,7 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, + Type = BufferBindingType.Storage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1763,15 +1479,73 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( Binding = 3, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-scatter bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Storage, HasDynamicOffset = false, MinBindingSize = 0 } }; - entries[4] = new BindGroupLayoutEntry + entries[2] = new BindGroupLayoutEntry { - Binding = 4, + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { @@ -1783,14 +1557,14 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, + EntryCount = 4, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite tile-scatter bind group layout."; + error = "Failed to create prepared composite tile-sort bind group layout."; return false; } @@ -1814,7 +1588,7 @@ private static bool TryCreateCompositionTexture( TextureDescriptor textureDescriptor = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.TextureBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, + Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, Dimension = TextureDimension.Dimension2D, Size = new Extent3D((uint)width, (uint)height, 1), Format = flushContext.TextureFormat, @@ -1862,14 +1636,19 @@ private static bool TryCreateCompositionTexture( private static void CopyTextureRegion( WebGPUFlushContext flushContext, Texture* sourceTexture, + int sourceOriginX, + int sourceOriginY, Texture* destinationTexture, - in Rectangle sourceRegion) + int destinationOriginX, + int destinationOriginY, + int width, + int height) { ImageCopyTexture source = new() { Texture = sourceTexture, MipLevel = 0, - Origin = new Origin3D((uint)sourceRegion.X, (uint)sourceRegion.Y, 0), + Origin = new Origin3D((uint)sourceOriginX, (uint)sourceOriginY, 0), Aspect = TextureAspect.All }; @@ -1877,11 +1656,11 @@ private static void CopyTextureRegion( { Texture = destinationTexture, MipLevel = 0, - Origin = new Origin3D(0, 0, 0), + Origin = new Origin3D((uint)destinationOriginX, (uint)destinationOriginY, 0), Aspect = TextureAspect.All }; - Extent3D copySize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + Extent3D copySize = new((uint)width, (uint)height, 1); flushContext.Api.CommandEncoderCopyTextureToTexture(flushContext.CommandEncoder, in source, in destination, in copySize); } @@ -1892,9 +1671,6 @@ private static void CopyTextureRegion( private static uint DivideRoundUp(int value, int divisor) => (uint)((value + divisor - 1) / divisor); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint AlignTo256(uint value) => (value + 255U) & ~255U; - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint FloatToUInt32Bits(float value) => unchecked((uint)System.BitConverter.SingleToInt32Bits(value)); @@ -2129,6 +1905,7 @@ public void Dispose() return; } + this.DisposeCoverageResources(); WebGPUFlushContext.ClearDeviceStateCache(); WebGPUFlushContext.ClearFallbackStagingCache(); @@ -2184,65 +1961,67 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.Wait(CallbackTimeoutMilliseconds); } - /// - /// Destination initialization parameters consumed by . - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct CompositeDestinationInitParameters + private readonly struct CoverageDefinitionIdentity : IEquatable { - public readonly int BatchWidth; - public readonly int BatchHeight; - public readonly int SourceOriginX; - public readonly int SourceOriginY; - - public CompositeDestinationInitParameters( - int batchWidth, - int batchHeight, - int sourceOriginX, - int sourceOriginY) - { - this.BatchWidth = batchWidth; - this.BatchHeight = batchHeight; - this.SourceOriginX = sourceOriginX; - this.SourceOriginY = sourceOriginY; - } - } - - /// - /// Destination blit parameters consumed by . - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct CompositeDestinationBlitParameters - { - public readonly int BatchWidth; - public readonly int BatchHeight; - public readonly int TargetOriginX; - public readonly int TargetOriginY; - - public CompositeDestinationBlitParameters( - int batchWidth, - int batchHeight, - int targetOriginX, - int targetOriginY) - { - this.BatchWidth = batchWidth; - this.BatchHeight = batchHeight; - this.TargetOriginX = targetOriginX; - this.TargetOriginY = targetOriginY; - } + private readonly int definitionKey; + private readonly IPath path; + private readonly Rectangle interest; + private readonly IntersectionRule intersectionRule; + private readonly RasterizationMode rasterizationMode; + private readonly RasterizerSamplingOrigin samplingOrigin; + + public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition) + { + this.definitionKey = definition.DefinitionKey; + this.path = definition.Path; + this.interest = definition.RasterizerOptions.Interest; + this.intersectionRule = definition.RasterizerOptions.IntersectionRule; + this.rasterizationMode = definition.RasterizerOptions.RasterizationMode; + this.samplingOrigin = definition.RasterizerOptions.SamplingOrigin; + } + + public bool Equals(CoverageDefinitionIdentity other) + => this.definitionKey == other.definitionKey && + ReferenceEquals(this.path, other.path) && + this.interest.Equals(other.interest) && + this.intersectionRule == other.intersectionRule && + this.rasterizationMode == other.rasterizationMode && + this.samplingOrigin == other.samplingOrigin; + + public override bool Equals(object? obj) + => obj is CoverageDefinitionIdentity other && this.Equals(other); + + public override int GetHashCode() + => HashCode.Combine( + this.definitionKey, + RuntimeHelpers.GetHashCode(this.path), + this.interest, + (int)this.intersectionRule, + (int)this.rasterizationMode, + (int)this.samplingOrigin); } - private readonly struct PreparedCompositePendingCommand + private readonly struct FlushCompositeCommand { - public PreparedCompositePendingCommand(int coverageDefinitionIndex, in PreparedCompositionCommand command) + public FlushCompositeCommand( + int coverageDefinitionIndex, + in PreparedCompositionCommand command, + in Rectangle destinationRegion, + in Point sourceOffset) { this.CoverageDefinitionIndex = coverageDefinitionIndex; this.Command = command; + this.DestinationRegion = destinationRegion; + this.SourceOffset = sourceOffset; } public int CoverageDefinitionIndex { get; } public PreparedCompositionCommand Command { get; } + + public Rectangle DestinationRegion { get; } + + public Point SourceOffset { get; } } private readonly struct CoveragePlacement @@ -2273,8 +2052,10 @@ private readonly struct PreparedCompositeDispatchConfig public readonly uint TileCountY; public readonly uint TileCount; public readonly uint CommandCount; - public readonly uint Pad0; - public readonly uint Pad1; + public readonly uint SourceOriginX; + public readonly uint SourceOriginY; + public readonly uint OutputOriginX; + public readonly uint OutputOriginY; public PreparedCompositeDispatchConfig( uint targetWidth, @@ -2282,7 +2063,11 @@ public PreparedCompositeDispatchConfig( uint tileCountX, uint tileCountY, uint tileCount, - uint commandCount) + uint commandCount, + uint sourceOriginX, + uint sourceOriginY, + uint outputOriginX, + uint outputOriginY) { this.TargetWidth = targetWidth; this.TargetHeight = targetHeight; @@ -2290,13 +2075,15 @@ public PreparedCompositeDispatchConfig( this.TileCountY = tileCountY; this.TileCount = tileCount; this.CommandCount = commandCount; - this.Pad0 = 0; - this.Pad1 = 0; + this.SourceOriginX = sourceOriginX; + this.SourceOriginY = sourceOriginY; + this.OutputOriginX = outputOriginX; + this.OutputOriginY = outputOriginY; } } /// - /// Prepared composite command parameters consumed by . + /// Prepared composite command parameters consumed by . /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeParameters @@ -2322,6 +2109,8 @@ private readonly struct PreparedCompositeParameters public readonly uint SolidG; public readonly uint SolidB; public readonly uint SolidA; + public readonly uint TileEmitOffset; + public readonly uint TileEmitCount; public PreparedCompositeParameters( int destinationX, @@ -2341,7 +2130,9 @@ public PreparedCompositeParameters( uint colorBlendMode, uint alphaCompositionMode, float blendPercentage, - Vector4 solidColor) + Vector4 solidColor, + uint tileEmitOffset, + uint tileEmitCount) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -2364,6 +2155,8 @@ public PreparedCompositeParameters( this.SolidG = FloatToUInt32Bits(solidColor.Y); this.SolidB = FloatToUInt32Bits(solidColor.Z); this.SolidA = FloatToUInt32Bits(solidColor.W); + this.TileEmitOffset = tileEmitOffset; + this.TileEmitCount = tileEmitCount; } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 7016ea068..7561ef713 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -103,27 +103,6 @@ private WebGPUFlushContext( public nuint InstanceBufferWriteOffset { get; internal set; } - /// - /// Gets or sets the flush-scoped destination pixel buffer used by composition compute shaders. - /// This buffer is initialized once per flush from the target texture and reused across composition batches. - /// - public WgpuBuffer* CompositeDestinationPixelsBuffer { get; internal set; } - - /// - /// Gets or sets the byte size of . - /// - public nuint CompositeDestinationPixelsByteSize { get; internal set; } - - /// - /// Gets or sets the destination buffer width represented by . - /// - public int CompositeDestinationWidth { get; internal set; } - - /// - /// Gets or sets the destination buffer height represented by . - /// - public int CompositeDestinationHeight { get; internal set; } - public CommandEncoder* CommandEncoder { get; set; } public RenderPassEncoder* PassEncoder { get; private set; } @@ -490,10 +469,6 @@ public void Dispose() this.ReadbackBuffer = null; this.TargetView = null; this.TargetTexture = null; - this.CompositeDestinationPixelsBuffer = null; - this.CompositeDestinationPixelsByteSize = 0; - this.CompositeDestinationWidth = 0; - this.CompositeDestinationHeight = 0; this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; this.RequiresReadback = false; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs index bf7671709..0c14037da 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -58,7 +58,7 @@ internal static bool TryCreate( TextureDescriptor targetTextureDescriptor = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding, Dimension = TextureDimension.Dimension2D, Size = new Extent3D((uint)width, (uint)height, 1), Format = textureFormat, From 1d41bac22ce2cacaf6063a2daf43e3b1086b2a08 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2026 12:33:41 +1000 Subject: [PATCH 029/136] Implement binning-based composite pipeline --- .../PreparedCompositeBinningComputeShader.cs | 168 ++++ .../PreparedCompositeFineComputeShader.cs | 8 +- ...PreparedCompositeTileCountComputeShader.cs | 135 ++- .../PreparedCompositeTileFillComputeShader.cs | 112 +++ ...reparedCompositeTilePrefixComputeShader.cs | 29 +- ...eparedCompositeTileScatterComputeShader.cs | 114 --- .../PreparedCompositeTileSortComputeShader.cs | 75 -- .../WEBGPU_BACKEND_PROCESS.md | 10 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 104 +- .../WebGPUDrawingBackend.cs | 890 +++++++++++------- .../Drawing/DrawTextRepeatedGlyphs.cs | 2 +- 11 files changed, 1027 insertions(+), 620 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs new file mode 100644 index 000000000..010c023f7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeBinningComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, + }; + + struct CommandBbox { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + }; + + struct BinHeader { + element_count: u32, + chunk_offset: u32, + }; + + struct BumpAllocators { + failed: atomic, + binning: atomic, + }; + + @group(0) @binding(0) var command_bboxes: array; + @group(0) @binding(1) var bin_header: array; + @group(0) @binding(2) var bin_data: array; + @group(0) @binding(3) var bump: BumpAllocators; + @group(0) @binding(4) var dispatch_config: DispatchConfig; + + const TILE_WIDTH: u32 = 16u; + const TILE_HEIGHT: u32 = 16u; + const N_TILE_X: u32 = 16u; + const N_TILE_Y: u32 = 16u; + const N_TILE: u32 = N_TILE_X * N_TILE_Y; + const WG_SIZE: u32 = 256u; + const N_SLICE: u32 = WG_SIZE / 32u; + const N_SUBSLICE: u32 = 4u; + const SX: f32 = 1.0 / f32(N_TILE_X * TILE_WIDTH); + const SY: f32 = 1.0 / f32(N_TILE_Y * TILE_HEIGHT); + const STAGE_BINNING: u32 = 1u; + + var sh_bitmaps: array, N_TILE>, N_SLICE>; + var sh_count: array, N_SUBSLICE>; + var sh_chunk_offset: array; + + @compute @workgroup_size(256) + fn cs_main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + ) { + for (var i = 0u; i < N_SLICE; i += 1u) { + atomicStore(&sh_bitmaps[i][local_id.x], 0u); + } + workgroupBarrier(); + + let element_ix = global_id.x; + var x0 = 0; + var y0 = 0; + var x1 = 0; + var y1 = 0; + if (element_ix < dispatch_config.command_count) { + let bbox = command_bboxes[element_ix]; + let fbbox = vec4(vec4(bbox.x0, bbox.y0, bbox.x1, bbox.y1)); + if (fbbox.x < fbbox.z && fbbox.y < fbbox.w) { + x0 = i32(floor(fbbox.x * SX)); + y0 = i32(floor(fbbox.y * SY)); + x1 = i32(ceil(fbbox.z * SX)); + y1 = i32(ceil(fbbox.w * SY)); + } + } + + let width_in_bins = i32(dispatch_config.width_in_bins); + let height_in_bins = i32(dispatch_config.height_in_bins); + x0 = clamp(x0, 0, width_in_bins); + y0 = clamp(y0, 0, height_in_bins); + x1 = clamp(x1, 0, width_in_bins); + y1 = clamp(y1, 0, height_in_bins); + if (x0 == x1) { + y1 = y0; + } + + var x = x0; + var y = y0; + let my_slice = local_id.x / 32u; + let my_mask = 1u << (local_id.x & 31u); + while y < y1 { + atomicOr(&sh_bitmaps[my_slice][u32(y * width_in_bins + x)], my_mask); + x += 1; + if x == x1 { + x = x0; + y += 1; + } + } + + workgroupBarrier(); + + var element_count = 0u; + for (var i = 0u; i < N_SUBSLICE; i += 1u) { + element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u][local_id.x])); + let element_count_lo = element_count; + element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u + 1u][local_id.x])); + let element_count_hi = element_count; + let element_count_packed = element_count_lo | (element_count_hi << 16u); + sh_count[i][local_id.x] = element_count_packed; + } + + var chunk_offset = atomicAdd(&bump.binning, element_count); + if chunk_offset + element_count > dispatch_config.binning_size { + chunk_offset = 0u; + atomicOr(&bump.failed, STAGE_BINNING); + } + + sh_chunk_offset[local_id.x] = chunk_offset; + bin_header[global_id.x].element_count = element_count; + bin_header[global_id.x].chunk_offset = chunk_offset; + workgroupBarrier(); + + x = x0; + y = y0; + while y < y1 { + let bin_ix = u32(y * width_in_bins + x); + let out_mask = atomicLoad(&sh_bitmaps[my_slice][bin_ix]); + if (out_mask & my_mask) != 0u { + var idx = countOneBits(out_mask & (my_mask - 1u)); + if my_slice > 0u { + let count_ix = my_slice - 1u; + let count_packed = sh_count[count_ix / 2u][bin_ix]; + idx += (count_packed >> (16u * (count_ix & 1u))) & 0xffffu; + } + let offset = dispatch_config.bin_data_start + sh_chunk_offset[bin_ix]; + bin_data[offset + idx] = element_ix; + } + x += 1; + if x == x1 { + x = x0; + y += 1; + } + } + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs index 2d6d1240f..8052bd6a6 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs @@ -37,8 +37,6 @@ struct Params { solid_g: u32, solid_b: u32, solid_a: u32, - tile_emit_offset: u32, - tile_emit_count: u32, }; struct DispatchConfig { @@ -52,6 +50,12 @@ struct DispatchConfig { source_origin_y: u32, output_origin_x: u32, output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, }; @group(0) @binding(0) var coverage_texture: texture_2d; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs index 07b771863..2062dfa9d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs @@ -7,34 +7,7 @@ internal static class PreparedCompositeTileCountComputeShader { private static readonly byte[] CodeBytes = [ - .. - """ - struct Params { - destination_x: u32, - destination_y: u32, - destination_width: u32, - destination_height: u32, - coverage_offset_x: u32, - coverage_offset_y: u32, - target_width: u32, - brush_type: u32, - brush_origin_x: u32, - brush_origin_y: u32, - brush_region_x: u32, - brush_region_y: u32, - brush_region_width: u32, - brush_region_height: u32, - color_blend_mode: u32, - alpha_composition_mode: u32, - blend_percentage: u32, - solid_r: u32, - solid_g: u32, - solid_b: u32, - solid_a: u32, - tile_emit_offset: u32, - tile_emit_count: u32, - }; - + .. """ struct DispatchConfig { target_width: u32, target_height: u32, @@ -42,66 +15,90 @@ struct DispatchConfig { tile_count_y: u32, tile_count: u32, command_count: u32, - pad0: u32, - pad1: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, }; - @group(0) @binding(0) var commands: array; - @group(0) @binding(1) var tile_counts: array>; - @group(0) @binding(2) var dispatch_config: DispatchConfig; + struct CommandBbox { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + }; - @compute @workgroup_size(64, 1, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let command_index = global_id.x; - if (command_index >= dispatch_config.command_count) { - return; - } + struct BinHeader { + element_count: u32, + chunk_offset: u32, + }; - if (dispatch_config.tile_count_x == 0u || dispatch_config.tile_count_y == 0u) { - return; - } + @group(0) @binding(0) var command_bboxes: array; + @group(0) @binding(1) var bin_header: array; + @group(0) @binding(2) var bin_data: array; + @group(0) @binding(3) var tile_counts: array>; + @group(0) @binding(4) var dispatch_config: DispatchConfig; - let command = commands[command_index]; - if (command.destination_width == 0u || command.destination_height == 0u) { + const TILE_WIDTH: u32 = 16u; + const TILE_HEIGHT: u32 = 16u; + const N_TILE_X: u32 = 16u; + const N_TILE_Y: u32 = 16u; + const N_TILE: u32 = N_TILE_X * N_TILE_Y; + + @compute @workgroup_size(256) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, + ) { + let bin_x = wg_id.x; + let bin_y = wg_id.y; + if (bin_x >= dispatch_config.width_in_bins || bin_y >= dispatch_config.height_in_bins) { return; } - let destination_x = bitcast(command.destination_x); - let destination_y = bitcast(command.destination_y); - let destination_max_x = destination_x + i32(command.destination_width) - 1; - let destination_max_y = destination_y + i32(command.destination_height) - 1; - let min_tile_x = u32(max(0, destination_x / 16)); - let min_tile_y = u32(max(0, destination_y / 16)); - let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); - let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); - - if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { + let tile_x = local_id.x % N_TILE_X; + let tile_y = local_id.x / N_TILE_X; + let global_tile_x = bin_x * N_TILE_X + tile_x; + let global_tile_y = bin_y * N_TILE_Y + tile_y; + if (global_tile_x >= dispatch_config.tile_count_x || global_tile_y >= dispatch_config.tile_count_y) { return; } - let emit_count = command.tile_emit_count; - var emitted: u32 = 0u; - var tile_y = min_tile_y; + let tile_index = global_tile_y * dispatch_config.tile_count_x + global_tile_x; + let tile_min_x = i32(global_tile_x * TILE_WIDTH); + let tile_min_y = i32(global_tile_y * TILE_HEIGHT); + let tile_max_x = tile_min_x + i32(TILE_WIDTH); + let tile_max_y = tile_min_y + i32(TILE_HEIGHT); + let bin_ix = bin_y * dispatch_config.width_in_bins + bin_x; + + var count = 0u; + var part_ix = 0u; loop { - if (tile_y > max_tile_y || emitted >= emit_count) { + if (part_ix >= dispatch_config.partition_count) { break; } - let row_offset = tile_y * dispatch_config.tile_count_x; - var tile_x = min_tile_x; - loop { - if (tile_x > max_tile_x || emitted >= emit_count) { - break; + let header = bin_header[part_ix * N_TILE + bin_ix]; + let element_count = header.element_count; + let base = header.chunk_offset; + for (var i = 0u; i < element_count; i += 1u) { + let cmd_index = bin_data[dispatch_config.bin_data_start + base + i]; + let bbox = command_bboxes[cmd_index]; + if (bbox.x1 > tile_min_x && bbox.x0 < tile_max_x && bbox.y1 > tile_min_y && bbox.y0 < tile_max_y) { + count = count + 1u; } - - let tile_index = row_offset + tile_x; - _ = atomicAdd(&tile_counts[tile_index], 1u); - emitted += 1u; - tile_x += 1u; } - tile_y += 1u; + part_ix = part_ix + 1u; } + + atomicStore(&tile_counts[tile_index], count); } """u8, 0 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs new file mode 100644 index 000000000..d8f5dcd61 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class PreparedCompositeTileFillComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, + }; + + struct CommandBbox { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + }; + + struct BinHeader { + element_count: u32, + chunk_offset: u32, + }; + + @group(0) @binding(0) var command_bboxes: array; + @group(0) @binding(1) var bin_header: array; + @group(0) @binding(2) var bin_data: array; + @group(0) @binding(3) var tile_starts: array; + @group(0) @binding(4) var tile_counts: array>; + @group(0) @binding(5) var tile_command_indices: array; + @group(0) @binding(6) var dispatch_config: DispatchConfig; + + const TILE_WIDTH: u32 = 16u; + const TILE_HEIGHT: u32 = 16u; + const N_TILE_X: u32 = 16u; + const N_TILE_Y: u32 = 16u; + const N_TILE: u32 = N_TILE_X * N_TILE_Y; + + @compute @workgroup_size(256) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, + ) { + let bin_x = wg_id.x; + let bin_y = wg_id.y; + if (bin_x >= dispatch_config.width_in_bins || bin_y >= dispatch_config.height_in_bins) { + return; + } + + let tile_x = local_id.x % N_TILE_X; + let tile_y = local_id.x / N_TILE_X; + let global_tile_x = bin_x * N_TILE_X + tile_x; + let global_tile_y = bin_y * N_TILE_Y + tile_y; + if (global_tile_x >= dispatch_config.tile_count_x || global_tile_y >= dispatch_config.tile_count_y) { + return; + } + + let tile_index = global_tile_y * dispatch_config.tile_count_x + global_tile_x; + let tile_min_x = i32(global_tile_x * TILE_WIDTH); + let tile_min_y = i32(global_tile_y * TILE_HEIGHT); + let tile_max_x = tile_min_x + i32(TILE_WIDTH); + let tile_max_y = tile_min_y + i32(TILE_HEIGHT); + let bin_ix = bin_y * dispatch_config.width_in_bins + bin_x; + + let start = tile_starts[tile_index]; + var offset = 0u; + var part_ix = 0u; + loop { + if (part_ix >= dispatch_config.partition_count) { + break; + } + + let header = bin_header[part_ix * N_TILE + bin_ix]; + let element_count = header.element_count; + let base = header.chunk_offset; + for (var i = 0u; i < element_count; i += 1u) { + let cmd_index = bin_data[dispatch_config.bin_data_start + base + i]; + let bbox = command_bboxes[cmd_index]; + if (bbox.x1 > tile_min_x && bbox.x0 < tile_max_x && bbox.y1 > tile_min_y && bbox.y0 < tile_max_y) { + tile_command_indices[start + offset] = cmd_index; + offset = offset + 1u; + } + } + + part_ix = part_ix + 1u; + } + + atomicStore(&tile_counts[tile_index], offset); + } + """u8, + 0 + ]; + + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs index a196ddd7f..e554b1677 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs @@ -7,8 +7,7 @@ internal static class PreparedCompositeTilePrefixComputeShader { private static readonly byte[] CodeBytes = [ - .. - """ + .. """ struct DispatchConfig { target_width: u32, target_height: u32, @@ -16,11 +15,19 @@ struct DispatchConfig { tile_count_y: u32, tile_count: u32, command_count: u32, - pad0: u32, - pad1: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, }; - @group(0) @binding(0) var tile_counts: array>; + @group(0) @binding(0) var tile_counts: array>; @group(0) @binding(1) var tile_starts: array; @group(0) @binding(2) var dispatch_config: DispatchConfig; @@ -30,19 +37,17 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { return; } - var running: u32 = 0u; - var tile_index: u32 = 0u; + var sum = 0u; + var tile_index = 0u; loop { if (tile_index >= dispatch_config.tile_count) { break; } - - let tile_count = atomicLoad(&tile_counts[tile_index]); - tile_starts[tile_index] = running; - running = running + tile_count; + let count = atomicLoad(&tile_counts[tile_index]); + tile_starts[tile_index] = sum; + sum = sum + count; tile_index = tile_index + 1u; } - } """u8, 0 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs deleted file mode 100644 index ec748f5b2..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileScatterComputeShader.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal static class PreparedCompositeTileScatterComputeShader -{ - private static readonly byte[] CodeBytes = - [ - .. - """ - struct Params { - destination_x: u32, - destination_y: u32, - destination_width: u32, - destination_height: u32, - coverage_offset_x: u32, - coverage_offset_y: u32, - target_width: u32, - brush_type: u32, - brush_origin_x: u32, - brush_origin_y: u32, - brush_region_x: u32, - brush_region_y: u32, - brush_region_width: u32, - brush_region_height: u32, - color_blend_mode: u32, - alpha_composition_mode: u32, - blend_percentage: u32, - solid_r: u32, - solid_g: u32, - solid_b: u32, - solid_a: u32, - tile_emit_offset: u32, - tile_emit_count: u32, - }; - - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - }; - - @group(0) @binding(0) var commands: array; - @group(0) @binding(1) var tile_offsets: array>; - @group(0) @binding(2) var tile_command_indices: array; - @group(0) @binding(3) var dispatch_config: DispatchConfig; - - @compute @workgroup_size(64, 1, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let command_index = global_id.x; - if (command_index >= dispatch_config.command_count) { - return; - } - - if (dispatch_config.tile_count_x == 0u || dispatch_config.tile_count_y == 0u) { - return; - } - - let command = commands[command_index]; - if (command.destination_width == 0u || command.destination_height == 0u) { - return; - } - - let destination_x = bitcast(command.destination_x); - let destination_y = bitcast(command.destination_y); - let destination_max_x = destination_x + i32(command.destination_width) - 1; - let destination_max_y = destination_y + i32(command.destination_height) - 1; - let min_tile_x = u32(max(0, destination_x / 16)); - let min_tile_y = u32(max(0, destination_y / 16)); - let max_tile_x = u32(min(i32(dispatch_config.tile_count_x) - 1, destination_max_x / 16)); - let max_tile_y = u32(min(i32(dispatch_config.tile_count_y) - 1, destination_max_y / 16)); - if (max_tile_x < min_tile_x || max_tile_y < min_tile_y) { - return; - } - - let emit_count = command.tile_emit_count; - var emitted: u32 = 0u; - var tile_y = min_tile_y; - loop { - if (tile_y > max_tile_y || emitted >= emit_count) { - break; - } - - let row_offset = tile_y * dispatch_config.tile_count_x; - var tile_x = min_tile_x; - loop { - if (tile_x > max_tile_x || emitted >= emit_count) { - break; - } - - let tile_index = row_offset + tile_x; - let write_index = atomicAdd(&tile_offsets[tile_index], 1u); - tile_command_indices[write_index] = command_index; - emitted += 1u; - tile_x += 1u; - } - - tile_y += 1u; - } - } - """u8, - 0 - ]; - - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs deleted file mode 100644 index 2dd8c74c7..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileSortComputeShader.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -internal static class PreparedCompositeTileSortComputeShader -{ - private static readonly byte[] CodeBytes = - [ - .. - """ - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - }; - - @group(0) @binding(0) var tile_starts: array; - @group(0) @binding(1) var tile_counts: array>; - @group(0) @binding(2) var tile_command_indices: array; - @group(0) @binding(3) var dispatch_config: DispatchConfig; - - @compute @workgroup_size(1, 1, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let tile_index = global_id.x; - if (tile_index >= dispatch_config.tile_count) { - return; - } - - let start = tile_starts[tile_index]; - let count = atomicLoad(&tile_counts[tile_index]); - if (count <= 1u) { - return; - } - - var i: u32 = 1u; - loop { - if (i >= count) { - break; - } - - let key = tile_command_indices[start + i]; - var j: u32 = i; - loop { - if (j == 0u) { - break; - } - - let previous_index = start + j - 1u; - let previous_value = tile_command_indices[previous_index]; - if (previous_value <= key) { - break; - } - - tile_command_indices[start + j] = previous_value; - j = j - 1u; - } - - tile_command_indices[start + j] = key; - i = i + 1u; - } - } - """u8, - 0 - ]; - - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index b4e59def5..5c242fd8e 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -32,10 +32,10 @@ DrawingCanvasBatcher.Flush() 6) PathTiling 7) CoverageFine -> build one flush-scoped composite command stream - -> command-parallel tile count - -> tile prefix - -> command-parallel tile scatter - -> per-tile command index sort (ascending command_index) + -> command-parallel tile-pair init (sentinel) + -> command-parallel tile-pair emit + -> global tile-pair key sort by (tile_index, command_index) + -> tile span build (tileStarts/tileCounts/tileCommandIndices) -> run one fine composite dispatch (PreparedCompositeFineComputeShader) -> solid brush uses Color.ToScaledVector4() -> image brush samples Image texture directly @@ -77,4 +77,4 @@ Fallback is scene-scoped: All static WGSL shader sources are stored as null-terminated UTF-8 bytes (`U+0000` terminator at call site requirement), including: - coverage pipeline shaders (`PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine`) -- composition shaders (`PreparedCompositeTileCount`, `PreparedCompositeTilePrefix`, `PreparedCompositeTileScatter`, `PreparedCompositeTileSort`, `PreparedCompositeFine`) +- composition shaders (`PreparedCompositeTilePairInit`, `PreparedCompositeTileEmit`, `PreparedCompositeTilePairSort`, `PreparedCompositeTileBuild`, `PreparedCompositeFine`) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 5141c7e77..b85a6a100 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -30,6 +30,10 @@ internal sealed unsafe partial class WebGPUDrawingBackend private const int SegmentAllocWorkgroupSize = 256; private readonly Dictionary coverageGeometryCache = new(); + private IMemoryOwner? cachedCoverageLineUpload; + private int cachedCoverageLineLength; + private IMemoryOwner? cachedCoveragePathUpload; + private int cachedCoveragePathLength; private delegate uint BindGroupEntryWriter(Span entries); @@ -213,14 +217,15 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - fixed (byte* lineUploadPtr = lineUpload) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, + if (!this.TryUploadDirtyCoverageRange( + flushContext, lineBuffer, - 0, - lineUploadPtr, - (nuint)lineBufferBytes); + lineUpload, + ref this.cachedCoverageLineUpload, + ref this.cachedCoverageLineLength, + out error)) + { + return false; } int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); @@ -251,14 +256,15 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - fixed (byte* pathUploadPtr = pathUpload) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, + if (!this.TryUploadDirtyCoverageRange( + flushContext, pathBuffer, - 0, - pathUploadPtr, - (nuint)pathBufferBytes); + pathUpload, + ref this.cachedCoveragePathUpload, + ref this.cachedCoveragePathLength, + out error)) + { + return false; } int tileBufferBytes = checked(totalTileCount * TileStrideBytes); @@ -482,6 +488,70 @@ private static bool TryGetOrCreateCoverageBuffer( out _, out error); + private bool TryUploadDirtyCoverageRange( + WebGPUFlushContext flushContext, + WgpuBuffer* destinationBuffer, + ReadOnlySpan source, + ref IMemoryOwner? cachedOwner, + ref int cachedLength, + out string? error) + { + error = null; + if (source.Length == 0) + { + cachedLength = 0; + return true; + } + + if (cachedOwner is null || cachedOwner.Memory.Length < source.Length) + { + cachedOwner?.Dispose(); + cachedOwner = flushContext.MemoryAllocator.Allocate(source.Length); + cachedLength = 0; + } + + Span cached = cachedOwner.Memory.Span[..source.Length]; + int previousLength = cachedLength; + int commonLength = Math.Min(previousLength, source.Length); + + int firstDifferent = 0; + while (firstDifferent < commonLength && cached[firstDifferent] == source[firstDifferent]) + { + firstDifferent++; + } + + int uploadLength = 0; + if (firstDifferent < source.Length) + { + int lastDifferent = source.Length - 1; + while (lastDifferent >= firstDifferent && + lastDifferent < commonLength && + cached[lastDifferent] == source[lastDifferent]) + { + lastDifferent--; + } + + uploadLength = (lastDifferent - firstDifferent) + 1; + } + + if (uploadLength > 0) + { + fixed (byte* sourcePtr = source) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + destinationBuffer, + (nuint)firstDifferent, + sourcePtr + firstDifferent, + (nuint)uploadLength); + } + } + + source.CopyTo(cached); + cachedLength = source.Length; + return true; + } + private void DisposeCoverageResources() { foreach (CachedCoverageGeometry geometry in this.coverageGeometryCache.Values) @@ -490,6 +560,12 @@ private void DisposeCoverageResources() } this.coverageGeometryCache.Clear(); + this.cachedCoverageLineUpload?.Dispose(); + this.cachedCoverageLineUpload = null; + this.cachedCoverageLineLength = 0; + this.cachedCoveragePathUpload?.Dispose(); + this.cachedCoveragePathUpload = null; + this.cachedCoveragePathLength = 0; } private static bool TryBuildLineBuffer( diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 013d1caf0..79aa646d2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -44,14 +44,23 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; private const int CompositeTileCommandWorkgroupSize = 64; + private const int CompositeBinTileCountX = 16; + private const int CompositeBinTileCountY = 16; + private const int CompositeBinningWorkgroupSize = 256; + private const int CompositeBinWidth = CompositeTileWidth * CompositeBinTileCountX; + private const int CompositeBinHeight = CompositeTileHeight * CompositeBinTileCountY; private const uint PreparedBrushTypeSolid = 0; private const uint PreparedBrushTypeImage = 1; private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; + private const string PreparedCompositeCommandBboxesBufferKey = "prepared-composite/command-bboxes"; private const string PreparedCompositeTileCountsBufferKey = "prepared-composite/tile-counts"; private const string PreparedCompositeTileStartsBufferKey = "prepared-composite/tile-starts"; - private const string PreparedCompositeTileOffsetsBufferKey = "prepared-composite/tile-offsets"; private const string PreparedCompositeTileIndicesBufferKey = "prepared-composite/tile-indices"; + private const string PreparedCompositeBinHeaderBufferKey = "prepared-composite/bin-header"; + private const string PreparedCompositeBinDataBufferKey = "prepared-composite/bin-data"; + private const string PreparedCompositeBinningBumpBufferKey = "prepared-composite/binning-bump"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; + private const int UniformBufferOffsetAlignment = 256; private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; @@ -282,6 +291,7 @@ public void FlushCompositions( configuration, target.Bounds, compositionBounds.Value, + commandCount, out failure) && this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); } @@ -405,6 +415,7 @@ private bool TryRenderPreparedFlush( Configuration configuration, Rectangle targetBounds, Rectangle compositionBounds, + int commandCount, out string? error) where TPixel : unmanaged, IPixel { @@ -435,93 +446,99 @@ private bool TryRenderPreparedFlush( Texture* outputTexture = flushContext.TargetTexture; TextureView* outputTextureView = flushContext.TargetView; bool writesDirectlyToTarget = !flushContext.RequiresReadback; + bool copyOutputToTarget = !writesDirectlyToTarget; int outputOriginX = writesDirectlyToTarget ? targetLocalBounds.X : 0; int outputOriginY = writesDirectlyToTarget ? targetLocalBounds.Y : 0; - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out Texture* sourceTexture, - out backdropTextureView, - out error)) + if (writesDirectlyToTarget) { - return false; + backdropTextureView = flushContext.TargetView; + sourceOriginX = targetLocalBounds.X; + sourceOriginY = targetLocalBounds.Y; + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out outputTexture, + out outputTextureView, + out error)) + { + return false; + } + + outputOriginX = 0; + outputOriginY = 0; + copyOutputToTarget = true; } + else + { + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out Texture* sourceTexture, + out backdropTextureView, + out error)) + { + return false; + } - CopyTextureRegion( - flushContext, - flushContext.TargetTexture, - targetLocalBounds.X, - targetLocalBounds.Y, - sourceTexture, - 0, - 0, - targetLocalBounds.Width, - targetLocalBounds.Height); - sourceOriginX = 0; - sourceOriginY = 0; - - if (!writesDirectlyToTarget && - !TryCreateCompositionTexture( + CopyTextureRegion( flushContext, + flushContext.TargetTexture, + targetLocalBounds.X, + targetLocalBounds.Y, + sourceTexture, + 0, + 0, targetLocalBounds.Width, - targetLocalBounds.Height, - out outputTexture, - out outputTextureView, - out error)) - { - return false; + targetLocalBounds.Height); + sourceOriginX = 0; + sourceOriginY = 0; + if (!TryCreateCompositionTexture( + flushContext, + targetLocalBounds.Width, + targetLocalBounds.Height, + out outputTexture, + out outputTextureView, + out error)) + { + return false; + } + + outputOriginX = 0; + outputOriginY = 0; } List coverageDefinitions = new(); Dictionary coverageDefinitionIndexByKey = new(); - List flushCommands = new(); + int[] batchCoverageIndices = new int[preparedBatches.Count]; + for (int i = 0; i < batchCoverageIndices.Length; i++) + { + batchCoverageIndices[i] = -1; + } + for (int i = 0; i < preparedBatches.Count; i++) { CompositionBatch batch = preparedBatches[i]; - if (batch.Commands.Count == 0) + IReadOnlyList commands = batch.Commands; + if (commands.Count == 0) { continue; } - IReadOnlyList commands = batch.Commands; - bool sawVisibleCommand = false; - int coverageDefinitionIndex = -1; - for (int commandIndex = 0; commandIndex < commands.Count; commandIndex++) + CoverageDefinitionIdentity definitionIdentity = new(batch.Definition); + if (!coverageDefinitionIndexByKey.TryGetValue(definitionIdentity, out int coverageDefinitionIndex)) { - PreparedCompositionCommand command = commands[commandIndex]; - Rectangle clippedDestination = Rectangle.Intersect(command.DestinationRegion, targetLocalBounds); - if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) - { - continue; - } - - if (!sawVisibleCommand) - { - CoverageDefinitionIdentity definitionIdentity = new(batch.Definition); - if (!coverageDefinitionIndexByKey.TryGetValue(definitionIdentity, out coverageDefinitionIndex)) - { - coverageDefinitionIndex = coverageDefinitions.Count; - coverageDefinitions.Add(batch.Definition); - coverageDefinitionIndexByKey.Add(definitionIdentity, coverageDefinitionIndex); - } - - sawVisibleCommand = true; - } - - Point clippedSourceOffset = new( - command.SourceOffset.X + (clippedDestination.X - command.DestinationRegion.X), - command.SourceOffset.Y + (clippedDestination.Y - command.DestinationRegion.Y)); - flushCommands.Add(new FlushCompositeCommand(coverageDefinitionIndex, command, clippedDestination, clippedSourceOffset)); + coverageDefinitionIndex = coverageDefinitions.Count; + coverageDefinitions.Add(batch.Definition); + coverageDefinitionIndexByKey.Add(definitionIdentity, coverageDefinitionIndex); } - if (sawVisibleCommand) - { - this.TestingComputePathBatchCount++; - } + batchCoverageIndices[i] = coverageDefinitionIndex; + this.TestingComputePathBatchCount++; } - if (flushCommands.Count == 0) + if (commandCount == 0) { error = null; return true; @@ -548,7 +565,9 @@ private bool TryRenderPreparedFlush( sourceOriginY, outputOriginX, outputOriginY, - flushCommands, + preparedBatches, + batchCoverageIndices, + commandCount, coveragePlacements, coverageView, out error)) @@ -556,7 +575,7 @@ private bool TryRenderPreparedFlush( return false; } - if (!writesDirectlyToTarget) + if (copyOutputToTarget) { CopyTextureRegion( flushContext, @@ -584,14 +603,16 @@ private bool TryDispatchPreparedCompositeCommands( int sourceOriginY, int outputOriginX, int outputOriginY, - IReadOnlyList flushCommands, + List preparedBatches, + int[] batchCoverageIndices, + int commandCount, CoveragePlacement[] coveragePlacements, TextureView* coverageTextureView, out string? error) where TPixel : unmanaged, IPixel { error = null; - if (flushCommands.Count == 0) + if (commandCount == 0) { return true; } @@ -631,6 +652,9 @@ private bool TryDispatchPreparedCompositeCommands( int tileCountX = checked((int)DivideRoundUp(targetLocalBounds.Width, CompositeTileWidth)); int tileCountY = checked((int)DivideRoundUp(targetLocalBounds.Height, CompositeTileHeight)); int tileCount = checked(tileCountX * tileCountY); + int widthInBins = checked((int)DivideRoundUp(tileCountX, CompositeBinTileCountX)); + int heightInBins = checked((int)DivideRoundUp(tileCountY, CompositeBinTileCountY)); + int binCount = checked(widthInBins * heightInBins); if (tileCount == 0) { return true; @@ -638,90 +662,109 @@ private bool TryDispatchPreparedCompositeCommands( uint parameterSize = (uint)Unsafe.SizeOf(); IMemoryOwner parametersOwner = - flushContext.MemoryAllocator.Allocate(flushCommands.Count); + flushContext.MemoryAllocator.Allocate(commandCount); + IMemoryOwner bboxesOwner = + flushContext.MemoryAllocator.Allocate(commandCount); try { - int flushCommandCount = flushCommands.Count; - Span parameters = parametersOwner.Memory.Span[..flushCommands.Count]; + int flushCommandCount = commandCount; + Span parameters = parametersOwner.Memory.Span[..commandCount]; + Span commandBboxes = bboxesOwner.Memory.Span[..commandCount]; TextureView* brushTextureView = backdropTextureView; nint brushTextureViewHandle = (nint)backdropTextureView; bool hasImageTexture = false; - uint totalTilePairCount = 0; + uint maxTileCommandIndices = 0; + uint binningPairCount = 0; - for (int i = 0; i < flushCommandCount; i++) + int commandIndex = 0; + for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) { - FlushCompositeCommand flushCommand = flushCommands[i]; - PreparedCompositionCommand command = flushCommand.Command; - - uint brushType; - int brushOriginX = 0; - int brushOriginY = 0; - int brushRegionX = 0; - int brushRegionY = 0; - int brushRegionWidth = 1; - int brushRegionHeight = 1; - Vector4 solidColor = default; - - if (command.Brush is SolidBrush solidBrush) + int coverageDefinitionIndex = batchCoverageIndices[batchIndex]; + if (coverageDefinitionIndex < 0) { - brushType = PreparedBrushTypeSolid; - solidColor = solidBrush.Color.ToScaledVector4(); + continue; } - else if (command.Brush is ImageBrush imageBrush) + + IReadOnlyList commands = preparedBatches[batchIndex].Commands; + for (int i = 0; i < commands.Count; i++) { - brushType = PreparedBrushTypeImage; - Image image = (Image)imageBrush.SourceImage; - - if (!TryGetOrCreateImageTextureView( - flushContext, - image, - flushContext.TextureFormat, - out TextureView* resolvedBrushTextureView, - out error)) + PreparedCompositionCommand command = commands[i]; + + uint brushType; + int brushOriginX = 0; + int brushOriginY = 0; + int brushRegionX = 0; + int brushRegionY = 0; + int brushRegionWidth = 1; + int brushRegionHeight = 1; + Vector4 solidColor = default; + + if (command.Brush is SolidBrush solidBrush) { - return false; + brushType = PreparedBrushTypeSolid; + solidColor = solidBrush.Color.ToScaledVector4(); } - - if (!hasImageTexture) + else if (command.Brush is ImageBrush imageBrush) { - brushTextureView = resolvedBrushTextureView; - brushTextureViewHandle = (nint)resolvedBrushTextureView; - hasImageTexture = true; + brushType = PreparedBrushTypeImage; + Image image = (Image)imageBrush.SourceImage; + + if (!TryGetOrCreateImageTextureView( + flushContext, + image, + flushContext.TextureFormat, + out TextureView* resolvedBrushTextureView, + out error)) + { + return false; + } + + if (!hasImageTexture) + { + brushTextureView = resolvedBrushTextureView; + brushTextureViewHandle = (nint)resolvedBrushTextureView; + hasImageTexture = true; + } + else if (brushTextureViewHandle != (nint)resolvedBrushTextureView) + { + error = "Prepared composite flush currently supports one image brush texture per dispatch."; + return false; + } + + Rectangle sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)imageBrush.SourceRegion); + brushRegionX = sourceRegion.X; + brushRegionY = sourceRegion.Y; + brushRegionWidth = sourceRegion.Width; + brushRegionHeight = sourceRegion.Height; + brushOriginX = command.BrushBounds.X + imageBrush.Offset.X - targetBounds.X - targetLocalBounds.X; + brushOriginY = command.BrushBounds.Y + imageBrush.Offset.Y - targetBounds.Y - targetLocalBounds.Y; } - else if (brushTextureViewHandle != (nint)resolvedBrushTextureView) + else { - error = "Prepared composite flush currently supports one image brush texture per dispatch."; + error = "Unsupported brush type."; return false; } - Rectangle sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)imageBrush.SourceRegion); - brushRegionX = sourceRegion.X; - brushRegionY = sourceRegion.Y; - brushRegionWidth = sourceRegion.Width; - brushRegionHeight = sourceRegion.Height; - brushOriginX = command.BrushBounds.X + imageBrush.Offset.X - targetBounds.X - targetLocalBounds.X; - brushOriginY = command.BrushBounds.Y + imageBrush.Offset.Y - targetBounds.Y - targetLocalBounds.Y; - } - else - { - error = "Unsupported brush type."; - return false; - } - - CoveragePlacement coveragePlacement = coveragePlacements[flushCommand.CoverageDefinitionIndex]; - Rectangle destinationRegion = flushCommand.DestinationRegion; - Point sourceOffset = flushCommand.SourceOffset; - - int destinationX = destinationRegion.X - targetLocalBounds.X; - int destinationY = destinationRegion.Y - targetLocalBounds.Y; - int minTileX = destinationX / CompositeTileWidth; - int minTileY = destinationY / CompositeTileHeight; - int maxTileX = (destinationX + destinationRegion.Width - 1) / CompositeTileWidth; - int maxTileY = (destinationY + destinationRegion.Height - 1) / CompositeTileHeight; - uint tileEmitCount = checked((uint)((maxTileX - minTileX + 1) * (maxTileY - minTileY + 1))); - uint tileEmitOffset = totalTilePairCount; - totalTilePairCount = checked(totalTilePairCount + tileEmitCount); - PreparedCompositeParameters commandParameters = new( + CoveragePlacement coveragePlacement = coveragePlacements[coverageDefinitionIndex]; + Rectangle destinationRegion = command.DestinationRegion; + Point sourceOffset = command.SourceOffset; + + int destinationX = destinationRegion.X - targetLocalBounds.X; + int destinationY = destinationRegion.Y - targetLocalBounds.Y; + int minTileX = destinationX / CompositeTileWidth; + int minTileY = destinationY / CompositeTileHeight; + int maxTileX = (destinationX + destinationRegion.Width - 1) / CompositeTileWidth; + int maxTileY = (destinationY + destinationRegion.Height - 1) / CompositeTileHeight; + uint tileEmitCount = checked((uint)((maxTileX - minTileX + 1) * (maxTileY - minTileY + 1))); + maxTileCommandIndices = checked(maxTileCommandIndices + tileEmitCount); + + int minBinX = destinationX / CompositeBinWidth; + int minBinY = destinationY / CompositeBinHeight; + int maxBinX = (destinationX + destinationRegion.Width - 1) / CompositeBinWidth; + int maxBinY = (destinationY + destinationRegion.Height - 1) / CompositeBinHeight; + uint binEmitCount = checked((uint)((maxBinX - minBinX + 1) * (maxBinY - minBinY + 1))); + binningPairCount = checked(binningPairCount + binEmitCount); + PreparedCompositeParameters commandParameters = new( destinationX, destinationY, destinationRegion.Width, @@ -739,12 +782,17 @@ private bool TryDispatchPreparedCompositeCommands( (uint)command.GraphicsOptions.ColorBlendingMode, (uint)command.GraphicsOptions.AlphaCompositionMode, command.GraphicsOptions.BlendPercentage, - solidColor, - tileEmitOffset, - tileEmitCount); - - parameters[i] = commandParameters; + solidColor); + + parameters[commandIndex] = commandParameters; + commandBboxes[commandIndex] = new PreparedCompositeCommandBbox( + destinationX, + destinationY, + destinationX + destinationRegion.Width, + destinationY + destinationRegion.Height); + commandIndex++; } + } int usedParameterByteCount = checked(flushCommandCount * (int)parameterSize); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( @@ -768,12 +816,60 @@ private bool TryDispatchPreparedCompositeCommands( (nuint)usedParameterByteCount); } - int tileCountsByteCount = checked(tileCount * sizeof(uint)); + int usedCommandBboxByteCount = checked(flushCommandCount * Unsafe.SizeOf()); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileCountsBufferKey, + PreparedCompositeCommandBboxesBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileCountsByteCount, - out WgpuBuffer* tileCountsBuffer, + (nuint)usedCommandBboxByteCount, + out WgpuBuffer* commandBboxesBuffer, + out _, + out error)) + { + return false; + } + + fixed (PreparedCompositeCommandBbox* usedBboxesPtr = commandBboxes) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + commandBboxesBuffer, + 0, + usedBboxesPtr, + (nuint)usedCommandBboxByteCount); + } + + int partitionCount = (int)DivideRoundUp(flushCommandCount, CompositeBinningWorkgroupSize); + uint binningSize = Math.Max(binningPairCount, 1u); + int binHeaderCount = checked(partitionCount * CompositeBinningWorkgroupSize); + int binHeaderByteCount = checked(binHeaderCount * Unsafe.SizeOf()); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeBinHeaderBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)binHeaderByteCount, + out WgpuBuffer* binHeaderBuffer, + out _, + out error)) + { + return false; + } + + nuint binDataByteCount = checked((nuint)binningSize * (nuint)sizeof(uint)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeBinDataBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + binDataByteCount, + out WgpuBuffer* binDataBuffer, + out _, + out error)) + { + return false; + } + + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeBinningBumpBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + (nuint)Unsafe.SizeOf(), + out WgpuBuffer* binningBumpBuffer, out _, out error)) { @@ -782,14 +878,14 @@ private bool TryDispatchPreparedCompositeCommands( flushContext.Api.CommandEncoderClearBuffer( flushContext.CommandEncoder, - tileCountsBuffer, + binningBumpBuffer, 0, - (nuint)tileCountsByteCount); + (nuint)Unsafe.SizeOf()); int tileStartsByteCount = checked(tileCount * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeTileStartsBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst | BufferUsage.CopySrc, + BufferUsage.Storage | BufferUsage.CopyDst, (nuint)tileStartsByteCount, out WgpuBuffer* tileStartsBuffer, out _, @@ -798,29 +894,38 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - if (totalTilePairCount == 0) - { - error = null; - return true; - } - - nuint tileCommandIndicesByteCount = checked((nuint)totalTilePairCount * (nuint)sizeof(uint)); + int tileCountsByteCount = checked(tileCount * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileIndicesBufferKey, + PreparedCompositeTileCountsBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, - tileCommandIndicesByteCount, - out WgpuBuffer* tileCommandIndicesBuffer, + (nuint)tileCountsByteCount, + out WgpuBuffer* tileCountsBuffer, out _, out error)) { return false; } + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileStartsBuffer, + 0, + (nuint)tileStartsByteCount); + + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + tileCountsBuffer, + 0, + (nuint)tileCountsByteCount); + + uint tileCommandCapacity = maxTileCommandIndices; + nuint usedTileCommandCount = (nuint)Math.Max(tileCommandCapacity, 1u); + nuint tileCommandIndicesByteCount = checked(usedTileCommandCount * (nuint)sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileOffsetsBufferKey, + PreparedCompositeTileIndicesBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileStartsByteCount, - out WgpuBuffer* tileOffsetsBuffer, + tileCommandIndicesByteCount, + out WgpuBuffer* tileCommandIndicesBuffer, out _, out error)) { @@ -849,7 +954,13 @@ private bool TryDispatchPreparedCompositeCommands( (uint)sourceOriginX, (uint)sourceOriginY, (uint)outputOriginX, - (uint)outputOriginY); + (uint)outputOriginY, + (uint)widthInBins, + (uint)heightInBins, + (uint)binCount, + (uint)partitionCount, + binningSize, + 0u); flushContext.Api.QueueWriteBuffer( flushContext.Queue, dispatchConfigBuffer, @@ -857,57 +968,60 @@ private bool TryDispatchPreparedCompositeCommands( &dispatchConfig, dispatchConfigSize); - if (!this.DispatchPreparedCompositeTileCount( - flushContext, - paramsBuffer, - tileCountsBuffer, - dispatchConfigBuffer, - (uint)flushCommandCount, - out error)) - { - return false; - } - - if (!this.DispatchPreparedCompositeTilePrefix( - flushContext, - tileCountsBuffer, - tileStartsBuffer, - dispatchConfigBuffer, - out error)) - { - return false; - } + if (tileCommandCapacity > 0 && flushCommandCount > 0) + { + if (!this.DispatchPreparedCompositeBinning( + flushContext, + commandBboxesBuffer, + binHeaderBuffer, + binDataBuffer, + binningBumpBuffer, + dispatchConfigBuffer, + flushCommandCount, + out error)) + { + return false; + } - flushContext.Api.CommandEncoderCopyBufferToBuffer( - flushContext.CommandEncoder, - tileStartsBuffer, - 0, - tileOffsetsBuffer, - 0, - (nuint)tileStartsByteCount); + if (!this.DispatchPreparedCompositeTileCount( + flushContext, + commandBboxesBuffer, + binHeaderBuffer, + binDataBuffer, + tileCountsBuffer, + dispatchConfigBuffer, + widthInBins, + heightInBins, + out error)) + { + return false; + } - if (!this.DispatchPreparedCompositeTileScatter( - flushContext, - paramsBuffer, - tileOffsetsBuffer, - tileCommandIndicesBuffer, - dispatchConfigBuffer, - (uint)flushCommandCount, - out error)) - { - return false; - } + if (!this.DispatchPreparedCompositeTilePrefix( + flushContext, + tileCountsBuffer, + tileStartsBuffer, + dispatchConfigBuffer, + out error)) + { + return false; + } - if (!this.DispatchPreparedCompositeTileSort( - flushContext, - tileStartsBuffer, - tileCountsBuffer, - tileCommandIndicesBuffer, - dispatchConfigBuffer, - (uint)tileCount, - out error)) - { - return false; + if (!this.DispatchPreparedCompositeTileFill( + flushContext, + commandBboxesBuffer, + binHeaderBuffer, + binDataBuffer, + tileStartsBuffer, + tileCountsBuffer, + tileCommandIndicesBuffer, + dispatchConfigBuffer, + widthInBins, + heightInBins, + out error)) + { + return false; + } } BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[9]; @@ -1009,6 +1123,7 @@ private bool TryDispatchPreparedCompositeCommands( finally { parametersOwner.Dispose(); + bboxesOwner.Dispose(); } error = null; @@ -1017,10 +1132,13 @@ private bool TryDispatchPreparedCompositeCommands( private bool DispatchPreparedCompositeTileCount( WebGPUFlushContext flushContext, - WgpuBuffer* paramsBuffer, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, WgpuBuffer* tileCountsBuffer, WgpuBuffer* dispatchConfigBuffer, - uint commandCount, + int widthInBins, + int heightInBins, out string? error) => this.DispatchComputePass( flushContext, @@ -1029,97 +1147,115 @@ private bool DispatchPreparedCompositeTileCount( TryCreatePreparedCompositeTileCountBindGroupLayout, (entries) => { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = paramsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 3; + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 5; + }, + (pass) => + { + uint workgroupCountX = (uint)widthInBins; + uint workgroupCountY = (uint)heightInBins; + if (workgroupCountX > 0 && workgroupCountY > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); + } }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( - pass, - DivideRoundUp(checked((int)commandCount), CompositeTileCommandWorkgroupSize), - 1, - 1), out error); - private bool DispatchPreparedCompositeTilePrefix( + private bool DispatchPreparedCompositeBinning( WebGPUFlushContext flushContext, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* tileStartsBuffer, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, + WgpuBuffer* binningBumpBuffer, WgpuBuffer* dispatchConfigBuffer, + int commandCount, out string? error) => this.DispatchComputePass( flushContext, - "prepared-composite-tile-prefix", - PreparedCompositeTilePrefixComputeShader.Code, - TryCreatePreparedCompositeTilePrefixBindGroupLayout, + "prepared-composite-binning", + PreparedCompositeBinningComputeShader.Code, + TryCreatePreparedCompositeBinningBindGroupLayout, (entries) => { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 3; + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = binningBumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 5; + }, + (pass) => + { + uint workgroupCount = DivideRoundUp(commandCount, CompositeBinningWorkgroupSize); + if (workgroupCount > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCount, 1, 1); + } }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( - pass, - 1, - 1, - 1), out error); - private bool DispatchPreparedCompositeTileScatter( + private bool DispatchPreparedCompositeTilePrefix( WebGPUFlushContext flushContext, - WgpuBuffer* paramsBuffer, - WgpuBuffer* tileOffsetsBuffer, - WgpuBuffer* tileCommandIndicesBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* tileStartsBuffer, WgpuBuffer* dispatchConfigBuffer, - uint commandCount, out string? error) => this.DispatchComputePass( flushContext, - "prepared-composite-tile-scatter", - PreparedCompositeTileScatterComputeShader.Code, - TryCreatePreparedCompositeTileScatterBindGroupLayout, + "prepared-composite-tile-prefix", + PreparedCompositeTilePrefixComputeShader.Code, + TryCreatePreparedCompositeTilePrefixBindGroupLayout, (entries) => { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = paramsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 4; + entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 3; }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( - pass, - DivideRoundUp(checked((int)commandCount), CompositeTileCommandWorkgroupSize), - 1, - 1), + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), out error); - private bool DispatchPreparedCompositeTileSort( + private bool DispatchPreparedCompositeTileFill( WebGPUFlushContext flushContext, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, WgpuBuffer* tileStartsBuffer, WgpuBuffer* tileCountsBuffer, WgpuBuffer* tileCommandIndicesBuffer, WgpuBuffer* dispatchConfigBuffer, - uint tileCount, + int widthInBins, + int heightInBins, out string? error) => this.DispatchComputePass( flushContext, - "prepared-composite-tile-sort", - PreparedCompositeTileSortComputeShader.Code, - TryCreatePreparedCompositeTileSortBindGroupLayout, + "prepared-composite-tile-fill", + PreparedCompositeTileFillComputeShader.Code, + TryCreatePreparedCompositeTileFillBindGroupLayout, (entries) => { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 4; + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[5] = new BindGroupEntry { Binding = 5, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[6] = new BindGroupEntry { Binding = 6, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 7; + }, + (pass) => + { + uint workgroupCountX = (uint)widthInBins; + uint workgroupCountY = (uint)heightInBins; + if (workgroupCountX > 0 && workgroupCountY > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); + } }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups( - pass, - tileCount, - 1, - 1), out error); private static bool TryGetOrCreateImageTextureView( @@ -1324,7 +1460,7 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1342,7 +1478,7 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1352,6 +1488,28 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( Binding = 2, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1361,7 +1519,7 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 3, + EntryCount = 5, Entries = entries }; @@ -1376,20 +1534,20 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( + private static bool TryCreatePreparedCompositeBinningBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; entries[0] = new BindGroupLayoutEntry { Binding = 0, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1410,6 +1568,28 @@ private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( Binding = 2, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1419,14 +1599,14 @@ private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 3, + EntryCount = 5, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite tile-prefix bind group layout."; + error = "Failed to create prepared composite binning bind group layout."; return false; } @@ -1434,13 +1614,13 @@ private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( + private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1468,17 +1648,6 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( Binding = 2, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1488,14 +1657,14 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, + EntryCount = 3, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite tile-scatter bind group layout."; + error = "Failed to create prepared composite tile-prefix bind group layout."; return false; } @@ -1503,13 +1672,13 @@ private static bool TryCreatePreparedCompositeTileScatterBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( + private static bool TryCreatePreparedCompositeTileFillBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1527,7 +1696,7 @@ private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1538,7 +1707,7 @@ private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } @@ -1548,6 +1717,39 @@ private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( Binding = 3, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[4] = new BindGroupLayoutEntry + { + Binding = 4, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[5] = new BindGroupLayoutEntry + { + Binding = 5, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[6] = new BindGroupLayoutEntry + { + Binding = 6, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, @@ -1557,14 +1759,14 @@ private static bool TryCreatePreparedCompositeTileSortBindGroupLayout( BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, + EntryCount = 7, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite tile-sort bind group layout."; + error = "Failed to create prepared composite tile-fill bind group layout."; return false; } @@ -2001,29 +2203,6 @@ public override int GetHashCode() (int)this.samplingOrigin); } - private readonly struct FlushCompositeCommand - { - public FlushCompositeCommand( - int coverageDefinitionIndex, - in PreparedCompositionCommand command, - in Rectangle destinationRegion, - in Point sourceOffset) - { - this.CoverageDefinitionIndex = coverageDefinitionIndex; - this.Command = command; - this.DestinationRegion = destinationRegion; - this.SourceOffset = sourceOffset; - } - - public int CoverageDefinitionIndex { get; } - - public PreparedCompositionCommand Command { get; } - - public Rectangle DestinationRegion { get; } - - public Point SourceOffset { get; } - } - private readonly struct CoveragePlacement { public CoveragePlacement(int originX, int originY, int width, int height) @@ -2056,6 +2235,12 @@ private readonly struct PreparedCompositeDispatchConfig public readonly uint SourceOriginY; public readonly uint OutputOriginX; public readonly uint OutputOriginY; + public readonly uint WidthInBins; + public readonly uint HeightInBins; + public readonly uint BinCount; + public readonly uint PartitionCount; + public readonly uint BinningSize; + public readonly uint BinDataStart; public PreparedCompositeDispatchConfig( uint targetWidth, @@ -2067,7 +2252,13 @@ public PreparedCompositeDispatchConfig( uint sourceOriginX, uint sourceOriginY, uint outputOriginX, - uint outputOriginY) + uint outputOriginY, + uint widthInBins, + uint heightInBins, + uint binCount, + uint partitionCount, + uint binningSize, + uint binDataStart) { this.TargetWidth = targetWidth; this.TargetHeight = targetHeight; @@ -2079,6 +2270,55 @@ public PreparedCompositeDispatchConfig( this.SourceOriginY = sourceOriginY; this.OutputOriginX = outputOriginX; this.OutputOriginY = outputOriginY; + this.WidthInBins = widthInBins; + this.HeightInBins = heightInBins; + this.BinCount = binCount; + this.PartitionCount = partitionCount; + this.BinningSize = binningSize; + this.BinDataStart = binDataStart; + } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeCommandBbox + { + public readonly int X0; + public readonly int Y0; + public readonly int X1; + public readonly int Y1; + + public PreparedCompositeCommandBbox(int x0, int y0, int x1, int y1) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeBinHeader + { + public readonly uint ElementCount; + public readonly uint ChunkOffset; + + public PreparedCompositeBinHeader(uint elementCount, uint chunkOffset) + { + this.ElementCount = elementCount; + this.ChunkOffset = chunkOffset; + } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct PreparedCompositeBinningBump + { + public readonly uint Failed; + public readonly uint Binning; + + public PreparedCompositeBinningBump(uint failed, uint binning) + { + this.Failed = failed; + this.Binning = binning; } } @@ -2109,8 +2349,6 @@ private readonly struct PreparedCompositeParameters public readonly uint SolidG; public readonly uint SolidB; public readonly uint SolidA; - public readonly uint TileEmitOffset; - public readonly uint TileEmitCount; public PreparedCompositeParameters( int destinationX, @@ -2130,9 +2368,7 @@ public PreparedCompositeParameters( uint colorBlendMode, uint alphaCompositionMode, float blendPercentage, - Vector4 solidColor, - uint tileEmitOffset, - uint tileEmitCount) + Vector4 solidColor) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -2155,8 +2391,6 @@ public PreparedCompositeParameters( this.SolidG = FloatToUInt32Bits(solidColor.Y); this.SolidB = FloatToUInt32Bits(solidColor.Z); this.SolidA = FloatToUInt32Bits(solidColor.W); - this.TileEmitOffset = tileEmitOffset; - this.TileEmitCount = tileEmitCount; } } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index d0a134742..3f1ed1934 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; [MemoryDiagnoser] [WarmupCount(5)] -[IterationCount(15)] +[IterationCount(5)] public class DrawTextRepeatedGlyphs { public const int Width = 1200; From 00f848e10444b981bd71b3fd95ab12afc18340c4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2026 13:53:29 +1000 Subject: [PATCH 030/136] Add docs and refactor WebGPU shaders/backend --- .../Shaders/BackdropComputeShader.cs | 6 +- .../Shaders/CompositeDestinationBlitShader.cs | Bin 3016 -> 0 bytes .../Shaders/CompositeDestinationInitShader.cs | Bin 2379 -> 0 bytes .../Shaders/CoverageFineComputeShader.cs | Bin 5561 -> 5759 bytes .../Shaders/PathCountComputeShader.cs | 12 +- .../Shaders/PathCountSetupComputeShader.cs | Bin 1761 -> 1949 bytes .../Shaders/PathTilingComputeShader.cs | Bin 7754 -> 7931 bytes .../Shaders/PathTilingSetupComputeShader.cs | Bin 1776 -> 1943 bytes .../PreparedCompositeBinningComputeShader.cs | 8 + .../PreparedCompositeFineComputeShader.cs | 45 ++-- ...PreparedCompositeTileCountComputeShader.cs | 7 + .../PreparedCompositeTileFillComputeShader.cs | 7 + ...reparedCompositeTilePrefixComputeShader.cs | 7 + .../Shaders/SegmentAllocComputeShader.cs | Bin 2105 -> 2295 bytes .../WEBGPU_BACKEND_PROCESS.md | 36 +-- .../WebGPUCompositeBindGroupLayoutFactory.cs | 2 +- .../WebGPUDrawingBackend.CompositePixels.cs | 9 + ...WebGPUDrawingBackend.CoverageRasterizer.cs | 234 +++++++++++++++--- .../WebGPUDrawingBackend.cs | 23 +- .../WebGPUFlushContext.cs | 203 +++++++++++++++ .../WebGPUTestNativeSurfaceAllocator.cs | 16 +- .../WebGPUTextureFormatMapper.cs | 13 + 22 files changed, 546 insertions(+), 82 deletions(-) delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationInitShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs index 4ae00664d..fd1043d98 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs @@ -4,10 +4,13 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Null-terminated WGSL compute shader for per-row backdrop prefix propagation. +/// Copies the destination texture into a composition backdrop for read-only sampling. /// internal static class BackdropComputeShader { + /// + /// Gets the null-terminated WGSL source for the backdrop copy pass. + /// private static readonly byte[] CodeBytes = [ .. @@ -110,5 +113,6 @@ fn cs_main( 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeDestinationBlitShader.cs deleted file mode 100644 index f2c2e4f3bb87f7a0be3c827215ecdeac4c57636e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3016 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 Y!f}ne+i8CYQlT3hGM&}+_fHq2AF@|)x&QzG delta 143 zcmeybvr~J*+$g`&oE+Vf)S}$Xyu^~!6oqj2U>}9#{M>@ll2nD_jKq}GB88HS#1e&~ z#A2Yb%&OF4h2qro+|<01Vuj4Sl6-~4JcS@*BR8O?vecr)^i+kC)QXbQqST4E-6zju ktYQsI&B@8%EWxx&Se$`N0Sc6qlu9kYyjW|l$u45n03%~CTL1t6 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs index 84e0759c2..ad717240e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs @@ -4,15 +4,18 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Null-terminated WGSL compute shader for path segment counting. +/// Counts paths per tile to size tile command lists. /// internal static class PathCountComputeShader { + /// + /// Gets the null-terminated WGSL source for the path count pass. + /// private static readonly byte[] CodeBytes = [ .. """ - // Path count stage (derived from Vello path_count.wgsl). + // Path count stage. const STAGE_BINNING: u32 = 0x1u; const STAGE_TILE_ALLOC: u32 = 0x2u; @@ -248,9 +251,10 @@ fn cs_main( } } } - - """u8 + """u8, + 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs index 4ede0c4ff00644a7dc3177ee61d78e9cdb8958cc..5c22c329ec1d59b248cc0cec610a2ceca29773a9 100644 GIT binary patch delta 312 zcmaFJJC}dLT%UlV)PlsK)MAB##F7k!45GmS3b$l97t6ry#Moc;Y=zD}8-^1)Jj1+}y;XN;@tEAOMNDgUtZy$Scjs(Je_W z%FWD6EJ;mK2zL+mQ7FzYElN(EctAl2rrt&$X2fJA#=_0}7%#HagAD^IP*PGVwa|fc z4Zy5eYc9BLaQorbB3T1}9#{M>@ll2nD_jKq}GqRBao3KL(u zPhQNJqphF;6v!+CDoiWN&s7LZ&B@7EC`c^Hh)>Qh%`1s7PAw@d&?`?b&e7b=&vcPR XoPkRL3Y3(TN-e;=SZl7yN$l1DE{!Ve diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs index 54ce2c083ec0e6abd262e9b7e7c3b390f9f47480..069d4e51f793c28f0393e6a252800554897035d3 100644 GIT binary patch delta 256 zcmX?Q^V@cVC}X<5zP^G@acORDVo{|XmjV!gMBGzLiWN#SQWf$_b8>V`Qj2mk^Abx^ zQxwA8gMAc=^Gl18Qx($ki@<6M5=$}^N-}dY^U@Ux5{rxV;KtbK!%Utmz?ivtK4YXz zJy;TCkCKv7sf7-lYXD}&T5}&N6p9NH Q^Yox@VFr40GQXTP02)e7s{jB1 delta 89 zcmexud&*{mDC6Wt#uN<&jg-`)%(B!Jg|wplT!paIoSb}xg2a-H_>#<=%)E5H^7P^y kO+Bv36PZLe|6z2J5oh31fC42YrBVwpFV>oC@&P$(0A92k8vpRTno><%#zH+oXo1!Vugalk_?5C%$&@;bcLeS;{4L0WT0qLYFd6#Do8w4 zAw4xOwJ5P9Ge2+QeJ?A0eSHO+;?msQ#G*<&E(IU}iMXeh6f2Zuq$=c<=H%#>q!#67 z<|US-rYMBF2m2^MOr3Z@Q3$5qMjvLxWEIAu&4(E8u+)PM11V5aQYy93fpZPOtXOL< zxNUIz;npHqqX2X{&?%Y43dI?TDXB#YiNy+u814!UanrR>C@x6M(}TK&8R(zMTiL7u D<8WS2 delta 131 zcmbQv|ABYHTxGw~oE+Vf)S}$Xyu^~!6oqj2U>}9#{M>@ll2nD_jKq}GqREkriW9GU zOrFS?tD~R+6v!+CDoiWN&s7LZ&B@7EC`c^Hh%d>^$;?ZSFHS8fEzm1ZFV4~2{EzVt Yi#P+90u(4IDV17)d9l`9lVjMe0db8i5dZ)H diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs index 010c023f7..391552b81 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs @@ -3,8 +3,15 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Bins prepared composite commands into coarse bins for later tile dispatch. +/// Produces per-bin headers and a compact bin list for the tile count/fill passes. +/// internal static class PreparedCompositeBinningComputeShader { + /// + /// Gets the null-terminated WGSL source for command binning. + /// private static readonly byte[] CodeBytes = [ .. """ @@ -164,5 +171,6 @@ fn cs_main( 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs index 8052bd6a6..9744e14e6 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs @@ -1,17 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; -using System.Collections.Generic; using System.Text; using Silk.NET.WebGPU; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Composites prepared commands over coverage in tile order to produce the final output. +/// Shader source is generated per texture format to match sampling/output requirements. +/// internal static class PreparedCompositeFineComputeShader { private static readonly object CacheSync = new(); - private static readonly Dictionary ShaderCache = new(); + private static readonly Dictionary ShaderCache = []; private static readonly string ShaderTemplate = """ @@ -281,6 +283,9 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { } """; + /// + /// Gets the input sample type required for the fine composite shader variant. + /// public static bool TryGetInputSampleType(TextureFormat textureFormat, out TextureSampleType sampleType) { if (TryGetTraits(textureFormat, out ShaderTraits traits)) @@ -293,18 +298,21 @@ public static bool TryGetInputSampleType(TextureFormat textureFormat, out Textur return false; } + /// + /// Gets the null-terminated WGSL source for the fine composite shader variant. + /// public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error) { if (!TryGetTraits(textureFormat, out ShaderTraits traits)) { - code = Array.Empty(); + code = []; error = $"Prepared composite fine shader does not support texture format '{textureFormat}'."; return false; } lock (CacheSync) { - if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode) && cachedCode is not null) + if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) { code = cachedCode; error = null; @@ -331,6 +339,9 @@ public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out return true; } + /// + /// Resolves shader traits for the provided texture format. + /// private static bool TryGetTraits(TextureFormat textureFormat, out ShaderTraits traits) { switch (textureFormat) @@ -394,14 +405,14 @@ private static bool TryGetTraits(TextureFormat textureFormat, out ShaderTraits t private static ShaderTraits CreateFloatTraits(string outputFormat) { - const string DecodeTexel = + const string decodeTexel = """ fn decode_texel(texel: vec4) -> vec4 { return texel; } """; - const string EncodeOutput = + const string encodeOutput = """ fn encode_output(color: vec4) -> vec4 { return color; @@ -412,8 +423,8 @@ fn encode_output(color: vec4) -> vec4 { outputFormat, "f32", TextureSampleType.Float, - DecodeTexel, - EncodeOutput, + decodeTexel, + encodeOutput, "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); @@ -421,14 +432,14 @@ fn encode_output(color: vec4) -> vec4 { private static ShaderTraits CreateSnormTraits(string outputFormat) { - const string DecodeTexel = + const string decodeTexel = """ fn decode_texel(texel: vec4) -> vec4 { return (texel * 0.5) + vec4(0.5); } """; - const string EncodeOutput = + const string encodeOutput = """ fn encode_output(color: vec4) -> vec4 { let clamped = clamp(color, vec4(0.0), vec4(1.0)); @@ -440,8 +451,8 @@ fn encode_output(color: vec4) -> vec4 { outputFormat, "f32", TextureSampleType.Float, - DecodeTexel, - EncodeOutput, + decodeTexel, + encodeOutput, "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); @@ -454,7 +465,7 @@ private static ShaderTraits CreateUintTraits(string outputFormat, float maxValue fn decode_texel(texel: vec4) -> vec4 {{ return vec4(texel) / UINT_TEXEL_MAX; }}"; - const string EncodeOutput = + const string encodeOutput = """ fn encode_output(color: vec4) -> vec4 { let clamped = clamp(color, vec4(0.0), vec4(1.0)); @@ -467,7 +478,7 @@ fn encode_output(color: vec4) -> vec4 { "u32", TextureSampleType.Uint, decodeTexel, - EncodeOutput, + encodeOutput, "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); @@ -483,7 +494,7 @@ private static ShaderTraits CreateSintTraits(string outputFormat, float minValue fn decode_texel(texel: vec4) -> vec4 {{ return (vec4(texel) - SINT_TEXEL_MIN) / SINT_TEXEL_RANGE; }}"; - const string EncodeOutput = + const string encodeOutput = """ fn encode_output(color: vec4) -> vec4 { let clamped = clamp(color, vec4(0.0), vec4(1.0)); @@ -496,7 +507,7 @@ fn encode_output(color: vec4) -> vec4 { "i32", TextureSampleType.Sint, decodeTexel, - EncodeOutput, + encodeOutput, "decode_texel(textureLoad(backdrop_texture, vec2(source_x, source_y), 0))", "decode_texel(textureLoad(brush_texture, vec2(sample_x, sample_y), 0))", "textureStore(output_texture, vec2(output_x_i32, output_y_i32), encode_output(vec4(rgb, alpha)));"); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs index 2062dfa9d..e90b02996 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs @@ -3,8 +3,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Counts the number of composite commands affecting each tile using bin headers. +/// internal static class PreparedCompositeTileCountComputeShader { + /// + /// Gets the null-terminated WGSL source for per-tile command counts. + /// private static readonly byte[] CodeBytes = [ .. """ @@ -104,5 +110,6 @@ fn cs_main( 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs index d8f5dcd61..784a17369 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs @@ -3,8 +3,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Expands per-bin command lists into per-tile command indices after prefix sizing. +/// internal static class PreparedCompositeTileFillComputeShader { + /// + /// Gets the null-terminated WGSL source for per-tile command index expansion. + /// private static readonly byte[] CodeBytes = [ .. """ @@ -108,5 +114,6 @@ fn cs_main( 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs index e554b1677..d3acb77e5 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs @@ -3,8 +3,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Prefix-sums per-tile command counts into tile starts for the fill pass. +/// internal static class PreparedCompositeTilePrefixComputeShader { + /// + /// Gets the null-terminated WGSL source for tile prefix sum calculation. + /// private static readonly byte[] CodeBytes = [ .. """ @@ -53,5 +59,6 @@ fn cs_main(@builtin(global_invocation_id) global_id: vec3) { 0 ]; + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. public static ReadOnlySpan Code => CodeBytes; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs index 585bd2a8eb1e32e9a056879d25b56d3cb8a37b97..feca6417d00303ce46e8a7c8ab48531711871743 100644 GIT binary patch delta 305 zcmdlf@Lh1iTocEfoc!d(lGI{_;?(rq)Vvaf;*$KL#Pn2!wEQB4wE|0S!-4 z2zL+mQ7FzYElP$M46`f|Vm;7MJ-8t@`Y>}hYccL+s|QPfY*12CDz(soa}B_(SZgkX yPMAixJJ9Vd$;bryG$S!3wMZecSRoO^p`jsex)ut>1&MijPo delta 118 zcmew^xKm)lTyMY9oE+Vf)S}$Xyu^~!6oqj2U>}9#{M>@ll2nD_jKq}GB89a4B89}9 zoc!d(lFYnxg@V)~-IB~4pmb__ZfahMLVj9WacW8N#Eb5mn;3Vni8F91K!K8yQmF-) M7i-Nmc@Kv*06u>w^8f$< diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 5c242fd8e..1e5a925d0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -15,11 +15,14 @@ DrawingCanvasBatcher.Flush() -> clip each command to target bounds -> group contiguous commands by DefinitionKey -> keep prepared destination/source offsets + -> compute scene command count + composition bounds + -> if no visible commands: return -> acquire one WebGPUFlushContext for the scene -> ensure command encoder (single encoder reused for the scene) -> resolve source backdrop texture view for composition bounds - -> source = target view when sampleable - -> else copy target region into transient composition texture and sample that + -> non-readback path: sample target view directly + -> readback path: copy target region into transient source texture and sample that + -> allocate transient output texture for composition -> build coverage texture from prepared geometry -> flatten prepared path geometry -> upload line/path/tile/segment buffers @@ -31,27 +34,27 @@ DrawingCanvasBatcher.Flush() 5) PathTilingSetup 6) PathTiling 7) CoverageFine - -> build one flush-scoped composite command stream - -> command-parallel tile-pair init (sentinel) - -> command-parallel tile-pair emit - -> global tile-pair key sort by (tile_index, command_index) - -> tile span build (tileStarts/tileCounts/tileCommandIndices) - -> run one fine composite dispatch (PreparedCompositeFineComputeShader) + -> build one flush-scoped composite command parameter stream from prepared batches + -> run composite dispatch sequence: + 1) PreparedCompositeBinning + 2) PreparedCompositeTileCount + 3) PreparedCompositeTilePrefix + 4) PreparedCompositeTileFill + 5) PreparedCompositeFine -> solid brush uses Color.ToScaledVector4() -> image brush samples Image texture directly -> writes composed pixels to one transient output texture -> copy output texture bounds back into the destination target once -> finalize once - -> finish encoder - -> single queue submit for the flush context - -> optional readback for CPU-region targets + -> non-readback: finish encoder + single queue submit + -> readback: encode texture->buffer copy, finish encoder + single queue submit, map/copy once -> on any GPU failure path: scene-scoped fallback (DefaultDrawingBackend) ``` ## Context and Resource Lifetime - `WebGPUFlushContext` is created once per `FlushCompositions` execution. -- The same command encoder is reused across all batch passes in that flush. +- The same command encoder is reused across all GPU passes in that flush. - Transient textures/buffers/bind-groups are tracked in the flush context and released on dispose. - Source image texture views are cached per flush context to avoid duplicate uploads. @@ -60,6 +63,7 @@ DrawingCanvasBatcher.Flush() - `FlushCompositions` performs one command-buffer submission (`QueueSubmit`) per scene flush. - Destination writeback to the render target is one copy from the fine output texture into composition bounds. - No destination storage init/blit pass is used in the active flush path. +- CPU-region targets perform one additional texture->buffer copy and one map/read after the single submit. ## Fallback Behavior @@ -74,7 +78,9 @@ Fallback is scene-scoped: ## Shader Source and Null Terminator -All static WGSL shader sources are stored as null-terminated UTF-8 bytes (`U+0000` terminator at call site requirement), including: +Static WGSL shaders are stored as null-terminated UTF-8 bytes (`U+0000` terminator required at call site), including: -- coverage pipeline shaders (`PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine`) -- composition shaders (`PreparedCompositeTilePairInit`, `PreparedCompositeTileEmit`, `PreparedCompositeTilePairSort`, `PreparedCompositeTileBuild`, `PreparedCompositeFine`) +- coverage shaders: `PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine` +- prepared-composite shaders: `PreparedCompositeBinning`, `PreparedCompositeTileCount`, `PreparedCompositeTilePrefix`, `PreparedCompositeTileFill` + +`PreparedCompositeFine` is generated per target texture format and emitted as null-terminated UTF-8 bytes at runtime. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs index de5b34edd..1d5ee20cf 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Creates a bind-group layout for one composite brush pipeline. +/// Creates a bind group layout for WebGPU composition pipelines. /// /// The WebGPU API facade. /// The device used to create resources. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index ad01234a2..74724186b 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -93,10 +93,19 @@ public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, i this.PixelSizeInBytes = pixelSizeInBytes; } + /// + /// Gets the CLR pixel type registered for this mapping. + /// public Type PixelType { get; } + /// + /// Gets the WebGPU texture format used for this pixel type. + /// public TextureFormat TextureFormat { get; } + /// + /// Gets the unmanaged size of the pixel type in bytes. + /// public int PixelSizeInBytes { get; } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index b85a6a100..cfbe15a45 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -1,15 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; using System.Buffers; using System.Buffers.Binary; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -29,16 +25,33 @@ internal sealed unsafe partial class WebGPUDrawingBackend private const int SegmentStrideBytes = 24; private const int SegmentAllocWorkgroupSize = 256; - private readonly Dictionary coverageGeometryCache = new(); + private readonly Dictionary coverageGeometryCache = []; private IMemoryOwner? cachedCoverageLineUpload; private int cachedCoverageLineLength; private IMemoryOwner? cachedCoveragePathUpload; private int cachedCoveragePathLength; + /// + /// Writes bind-group entries and returns the number of populated entries. + /// private delegate uint BindGroupEntryWriter(Span entries); - private unsafe delegate void ComputePassDispatch(ComputePassEncoder* pass); - + /// + /// Encapsulates dispatch logic for a compute pass. + /// + private delegate void ComputePassDispatch(ComputePassEncoder* pass); + + /// + /// Builds and dispatches the full coverage rasterization pipeline for flattened paths. + /// + /// The canvas pixel type. + /// The active flush context. + /// Coverage definitions participating in the current flush. + /// The current processing configuration. + /// Receives the output coverage texture view. + /// Receives per-definition atlas placement information. + /// Receives an error message when the operation fails. + /// when rasterization setup and dispatch succeed; otherwise . private bool TryCreateCoverageTextureFromFlattened( WebGPUFlushContext flushContext, List definitions, @@ -49,7 +62,7 @@ private bool TryCreateCoverageTextureFromFlattened( where TPixel : unmanaged, IPixel { coverageView = null; - coveragePlacements = Array.Empty(); + coveragePlacements = []; error = null; if (definitions.Count == 0) { @@ -66,6 +79,8 @@ private bool TryCreateCoverageTextureFromFlattened( int currentTileY = 0; uint? fillRuleValue = null; uint? aliasedValue = null; + + // First pass: validate inputs, resolve/build cached geometry, and pack atlas placements. for (int i = 0; i < definitions.Count; i++) { CompositionCoverageDefinition definition = definitions[i]; @@ -180,6 +195,7 @@ private bool TryCreateCoverageTextureFromFlattened( return true; } + // Build a merged line buffer with coordinates translated into atlas space. int lineBufferBytes = checked(totalLineCount * LineStrideBytes); using IMemoryOwner lineUploadOwner = configuration.MemoryAllocator.Allocate(lineBufferBytes); Span lineUpload = lineUploadOwner.Memory.Span[..lineBufferBytes]; @@ -228,6 +244,7 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } + // Build per-path metadata that maps each path into its tile span inside the atlas. int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; @@ -458,6 +475,7 @@ private bool TryCreateCoverageTextureFromFlattened( flushContext.Api.QueueWriteBuffer(flushContext.Queue, coverageConfigBuffer, 0, &coverageConfig, (nuint)Unsafe.SizeOf()); + // Dispatch compute stages in pipeline order: count -> backdrop -> alloc -> emit segments -> fine raster. if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error) || !this.DispatchPathCount(flushContext, configBuffer, bumpBuffer, lineBuffer, pathBuffer, tileBuffer, segCountsBuffer, indirectBuffer, out error) || !this.DispatchBackdrop(flushContext, configBuffer, tileBuffer, atlasHeightInTiles, out error) || @@ -473,6 +491,9 @@ private bool TryCreateCoverageTextureFromFlattened( return true; } + /// + /// Gets or creates a shared GPU buffer used by the coverage rasterization pipeline. + /// private static bool TryGetOrCreateCoverageBuffer( WebGPUFlushContext flushContext, string bufferKey, @@ -488,6 +509,9 @@ private static bool TryGetOrCreateCoverageBuffer( out _, out error); + /// + /// Uploads only the changed byte range of a coverage buffer payload. + /// private bool TryUploadDirtyCoverageRange( WebGPUFlushContext flushContext, WgpuBuffer* destinationBuffer, @@ -534,6 +558,7 @@ private bool TryUploadDirtyCoverageRange( uploadLength = (lastDifferent - firstDifferent) + 1; } + // Only write the dirty range to reduce queue upload bandwidth on repeated flushes. if (uploadLength > 0) { fixed (byte* sourcePtr = source) @@ -552,6 +577,9 @@ private bool TryUploadDirtyCoverageRange( return true; } + /// + /// Releases cached coverage resources and clears all CPU-side upload caches. + /// private void DisposeCoverageResources() { foreach (CachedCoverageGeometry geometry in this.coverageGeometryCache.Values) @@ -568,6 +596,9 @@ private void DisposeCoverageResources() this.cachedCoveragePathLength = 0; } + /// + /// Flattens a path into the compact line format consumed by coverage compute shaders. + /// private static bool TryBuildLineBuffer( IPath path, in Rectangle interest, @@ -699,9 +730,15 @@ private static bool TryBuildLineBuffer( return true; } + /// + /// Writes a single line record using the default path index. + /// private static void WriteLine(Span destination, int lineIndex, float x0, float y0, float x1, float y1) => WriteLine(destination, lineIndex, 0u, x0, y0, x1, y1); + /// + /// Writes a single line record with an explicit path index. + /// private static void WriteLine(Span destination, int lineIndex, uint pathIndex, float x0, float y0, float x1, float y1) { int offset = lineIndex * LineStrideBytes; @@ -713,12 +750,18 @@ private static void WriteLine(Span destination, int lineIndex, uint pathIn WriteFloat(destination, offset + 20, y1); } + /// + /// Writes a path bounding record using a default tile base. + /// private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1) => WritePath(destination, x0, y0, x1, y1, 0u); + /// + /// Writes a path bounding record with an explicit tile base offset. + /// private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1, uint tiles) { - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(0, 4), x0); + BinaryPrimitives.WriteUInt32LittleEndian(destination[..4], x0); BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4, 4), y0); BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8, 4), x1); BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), y1); @@ -728,13 +771,23 @@ private static void WritePath(Span destination, uint x0, uint y0, uint x1, BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(28, 4), 0u); } + /// + /// Reads a 32-bit floating-point value from a little-endian byte span. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float ReadFloat(ReadOnlySpan source, int offset) => BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(source.Slice(offset, 4))); + /// + /// Writes a 32-bit floating-point value to a little-endian byte span. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteFloat(Span destination, int offset, float value) => BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), (uint)BitConverter.SingleToInt32Bits(value)); + /// + /// Estimates how many tile segments a line contributes during path tiling. + /// private static uint EstimateSegmentCount(float x0, float y0, float x1, float y1) { float s0x = x0 * TileScale; @@ -751,6 +804,9 @@ private static uint EstimateSegmentCount(float x0, float y0, float x1, float y1) return countX + countY; } + /// + /// Computes the number of tiles spanned by two coordinates along one axis. + /// private static uint SpanTiles(float a, float b) { float max = MathF.Max(a, b); @@ -764,16 +820,9 @@ private static uint SpanTiles(float a, float b) return (uint)span; } - private static int Clamp(int value, int min, int max) - { - if (value < min) - { - return min; - } - - return value > max ? max : value; - } - + /// + /// Creates the coverage output texture and view used by the fine rasterization pass. + /// private static bool TryCreateCoverageTexture( WebGPUFlushContext flushContext, int width, @@ -860,6 +909,9 @@ private static bool TryCreateCoverageTexture( return true; } + /// + /// Dispatches the path-count setup shader that initializes indirect dispatch counts. + /// private bool DispatchPathCountSetup( WebGPUFlushContext flushContext, WgpuBuffer* bumpBuffer, @@ -879,6 +931,9 @@ private bool DispatchPathCountSetup( (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), out error); + /// + /// Dispatches the path-count shader that computes per-tile segment counts. + /// private bool DispatchPathCount( WebGPUFlushContext flushContext, WgpuBuffer* configBuffer, @@ -904,9 +959,12 @@ private bool DispatchPathCount( entries[5] = new BindGroupEntry { Binding = 5, Buffer = segCountsBuffer, Offset = 0, Size = nuint.MaxValue }; return 6; }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, (nuint)0), + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, 0), out error); + /// + /// Dispatches the segment-allocation shader that computes per-tile segment offsets. + /// private bool DispatchSegmentAlloc( WebGPUFlushContext flushContext, WgpuBuffer* bumpBuffer, @@ -935,6 +993,9 @@ private bool DispatchSegmentAlloc( }, out error); + /// + /// Dispatches the backdrop prefix shader that accumulates backdrop values across tile rows. + /// private bool DispatchBackdrop( WebGPUFlushContext flushContext, WgpuBuffer* configBuffer, @@ -952,12 +1013,12 @@ private bool DispatchBackdrop( entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; return 2; }, - (pass) => - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)heightInTiles, 1, 1); - }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)heightInTiles, 1, 1), out error); + /// + /// Dispatches the path-tiling setup shader that prepares indirect counts for segment emission. + /// private bool DispatchPathTilingSetup( WebGPUFlushContext flushContext, WgpuBuffer* bumpBuffer, @@ -977,6 +1038,9 @@ private bool DispatchPathTilingSetup( (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), out error); + /// + /// Dispatches the path-tiling shader that emits clipped segments into per-tile storage. + /// private bool DispatchPathTiling( WebGPUFlushContext flushContext, WgpuBuffer* bumpBuffer, @@ -1002,9 +1066,12 @@ private bool DispatchPathTiling( entries[5] = new BindGroupEntry { Binding = 5, Buffer = segmentsBuffer, Offset = 0, Size = nuint.MaxValue }; return 6; }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, (nuint)0), + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, 0), out error); + /// + /// Dispatches the fine coverage shader that rasterizes tile segments into the output texture. + /// private bool DispatchCoverageFine( WebGPUFlushContext flushContext, WgpuBuffer* coverageConfigBuffer, @@ -1029,12 +1096,12 @@ private bool DispatchCoverageFine( entries[4] = new BindGroupEntry { Binding = 4, TextureView = coverageView }; return 5; }, - (pass) => - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)tileWidth, (uint)tileHeight, 1); - }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)tileWidth, (uint)tileHeight, 1), out error); + /// + /// Creates and executes a compute pass for a coverage pipeline stage. + /// private bool DispatchComputePass( WebGPUFlushContext flushContext, string pipelineKey, @@ -1097,6 +1164,9 @@ private bool DispatchComputePass( return true; } + /// + /// Creates the bind-group layout used by the path-count setup shader. + /// private static bool TryCreatePathCountSetupBindGroupLayout( WebGPU api, Device* device, @@ -1144,6 +1214,9 @@ private static bool TryCreatePathCountSetupBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the path-count shader. + /// private static bool TryCreatePathCountBindGroupLayout( WebGPU api, Device* device, @@ -1181,7 +1254,7 @@ private static bool TryCreatePathCountBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)LineStrideBytes + MinBindingSize = LineStrideBytes } }; entries[3] = new BindGroupLayoutEntry @@ -1192,7 +1265,7 @@ private static bool TryCreatePathCountBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)PathStrideBytes + MinBindingSize = PathStrideBytes } }; entries[4] = new BindGroupLayoutEntry @@ -1203,7 +1276,7 @@ private static bool TryCreatePathCountBindGroupLayout( { Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = (nuint)TileStrideBytes + MinBindingSize = TileStrideBytes } }; entries[5] = new BindGroupLayoutEntry @@ -1214,7 +1287,7 @@ private static bool TryCreatePathCountBindGroupLayout( { Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = (nuint)SegmentCountStrideBytes + MinBindingSize = SegmentCountStrideBytes } }; @@ -1235,6 +1308,9 @@ private static bool TryCreatePathCountBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the segment-allocation shader. + /// private static bool TryCreateSegmentAllocBindGroupLayout( WebGPU api, Device* device, @@ -1261,7 +1337,7 @@ private static bool TryCreateSegmentAllocBindGroupLayout( { Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = (nuint)TileStrideBytes + MinBindingSize = TileStrideBytes } }; entries[2] = new BindGroupLayoutEntry @@ -1304,6 +1380,9 @@ private static bool TryCreateSegmentAllocBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the backdrop prefix shader. + /// private static bool TryCreateBackdropBindGroupLayout( WebGPU api, Device* device, @@ -1351,6 +1430,9 @@ private static bool TryCreateBackdropBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the path-tiling setup shader. + /// private static bool TryCreatePathTilingSetupBindGroupLayout( WebGPU api, Device* device, @@ -1398,6 +1480,9 @@ private static bool TryCreatePathTilingSetupBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the path-tiling shader. + /// private static bool TryCreatePathTilingBindGroupLayout( WebGPU api, Device* device, @@ -1424,7 +1509,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)SegmentCountStrideBytes + MinBindingSize = SegmentCountStrideBytes } }; entries[2] = new BindGroupLayoutEntry @@ -1435,7 +1520,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)LineStrideBytes + MinBindingSize = LineStrideBytes } }; entries[3] = new BindGroupLayoutEntry @@ -1446,7 +1531,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)PathStrideBytes + MinBindingSize = PathStrideBytes } }; entries[4] = new BindGroupLayoutEntry @@ -1457,7 +1542,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)TileStrideBytes + MinBindingSize = TileStrideBytes } }; entries[5] = new BindGroupLayoutEntry @@ -1468,7 +1553,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = (nuint)SegmentStrideBytes + MinBindingSize = SegmentStrideBytes } }; @@ -1489,6 +1574,9 @@ private static bool TryCreatePathTilingBindGroupLayout( return true; } + /// + /// Creates the bind-group layout used by the fine coverage shader. + /// private static bool TryCreateCoverageFineBindGroupLayout( WebGPU api, Device* device, @@ -1515,7 +1603,7 @@ private static bool TryCreateCoverageFineBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)TileStrideBytes + MinBindingSize = TileStrideBytes } }; entries[2] = new BindGroupLayoutEntry @@ -1537,7 +1625,7 @@ private static bool TryCreateCoverageFineBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = (nuint)SegmentStrideBytes + MinBindingSize = SegmentStrideBytes } }; entries[4] = new BindGroupLayoutEntry @@ -1569,8 +1657,14 @@ private static bool TryCreateCoverageFineBindGroupLayout( return true; } + /// + /// Flattened path payload used during coverage rasterization. + /// private readonly struct CoveragePathBuild { + /// + /// Initializes a new instance of the struct. + /// public CoveragePathBuild( CachedCoverageGeometry geometry, int originTileX, @@ -1585,17 +1679,35 @@ public CoveragePathBuild( this.OriginY = originY; } + /// + /// Gets the cached geometry payload. + /// public CachedCoverageGeometry Geometry { get; } + /// + /// Gets the atlas origin in tile coordinates on the X axis. + /// public int OriginTileX { get; } + /// + /// Gets the atlas origin in tile coordinates on the Y axis. + /// public int OriginTileY { get; } + /// + /// Gets the atlas origin in pixel coordinates on the X axis. + /// public int OriginX { get; } + /// + /// Gets the atlas origin in pixel coordinates on the Y axis. + /// public int OriginY { get; } } + /// + /// Rasterizer dispatch configuration for a coverage pass. + /// [StructLayout(LayoutKind.Sequential)] private struct RasterConfig { @@ -1625,6 +1737,9 @@ private struct RasterConfig public uint Pad1; } + /// + /// GPU bump allocator counters for transient coverage buffers. + /// [StructLayout(LayoutKind.Sequential)] private struct BumpAllocatorsData { @@ -1638,6 +1753,9 @@ private struct BumpAllocatorsData public uint Lines; } + /// + /// Indirect dispatch counts emitted by the coverage setup stage. + /// [StructLayout(LayoutKind.Sequential)] private struct IndirectCountData { @@ -1646,6 +1764,9 @@ private struct IndirectCountData public uint CountZ; } + /// + /// Segment allocator configuration for coverage path allocation. + /// [StructLayout(LayoutKind.Sequential)] private struct SegmentAllocConfig { @@ -1655,6 +1776,9 @@ private struct SegmentAllocConfig public uint Pad2; } + /// + /// Coverage pass configuration shared across compute stages. + /// [StructLayout(LayoutKind.Sequential)] private struct CoverageConfig { @@ -1668,8 +1792,14 @@ private struct CoverageConfig public uint IsAliased; } + /// + /// Cached CPU-side geometry payload reused across coverage flushes. + /// private sealed class CachedCoverageGeometry : IDisposable { + /// + /// Initializes a new instance of the class. + /// public CachedCoverageGeometry( IMemoryOwner? lineOwner, int lineCount, @@ -1688,20 +1818,42 @@ public CachedCoverageGeometry( this.CoverageHeight = coverageHeight; } + /// + /// Gets the owned line segment buffer for the cached coverage geometry. + /// public IMemoryOwner? LineOwner { get; } + /// + /// Gets the number of lines stored in . + /// public int LineCount { get; } + /// + /// Gets the estimated number of segments generated for this geometry. + /// public uint EstimatedSegments { get; } + /// + /// Gets the coverage width in tiles. + /// public int WidthInTiles { get; } + /// + /// Gets the coverage height in tiles. + /// public int HeightInTiles { get; } + /// + /// Gets the coverage texture width in pixels. + /// public int CoverageWidth { get; } + /// + /// Gets the coverage texture height in pixels. + /// public int CoverageHeight { get; } + /// public void Dispose() => this.LineOwner?.Dispose(); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 79aa646d2..ece0df386 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -43,7 +43,6 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi private const int CompositeComputeWorkgroupSize = 8; private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; - private const int CompositeTileCommandWorkgroupSize = 64; private const int CompositeBinTileCountX = 16; private const int CompositeBinTileCountY = 16; private const int CompositeBinningWorkgroupSize = 256; @@ -2163,6 +2162,9 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv return signal.Wait(CallbackTimeoutMilliseconds); } + /// + /// Key that identifies a coverage definition for reuse within a flush. + /// private readonly struct CoverageDefinitionIdentity : IEquatable { private readonly int definitionKey; @@ -2182,6 +2184,11 @@ public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition) this.samplingOrigin = definition.RasterizerOptions.SamplingOrigin; } + /// + /// Determines whether this identity equals the provided coverage identity. + /// + /// The identity to compare. + /// when the identities describe the same coverage definition; otherwise . public bool Equals(CoverageDefinitionIdentity other) => this.definitionKey == other.definitionKey && ReferenceEquals(this.path, other.path) && @@ -2190,9 +2197,11 @@ public bool Equals(CoverageDefinitionIdentity other) this.rasterizationMode == other.rasterizationMode && this.samplingOrigin == other.samplingOrigin; + /// public override bool Equals(object? obj) => obj is CoverageDefinitionIdentity other && this.Equals(other); + /// public override int GetHashCode() => HashCode.Combine( this.definitionKey, @@ -2222,6 +2231,9 @@ public CoveragePlacement(int originX, int originY, int width, int height) public int Height { get; } } + /// + /// Dispatch constants shared across composite compute passes. + /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeDispatchConfig { @@ -2279,6 +2291,9 @@ public PreparedCompositeDispatchConfig( } } + /// + /// Integer bounding box for a prepared composite command in destination-local coordinates. + /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeCommandBbox { @@ -2296,6 +2311,9 @@ public PreparedCompositeCommandBbox(int x0, int y0, int x1, int y1) } } + /// + /// Per-bin header describing the packed command list region. + /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeBinHeader { @@ -2309,6 +2327,9 @@ public PreparedCompositeBinHeader(uint elementCount, uint chunkOffset) } } + /// + /// Bump allocator state for command binning. + /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeBinningBump { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 7561ef713..58f8102a0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -12,8 +12,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Blend mode selection for render-pipeline-based composition passes. +/// internal enum CompositePipelineBlendMode { + /// + /// Uses default blending behavior for the render pipeline variant. + /// None = 0 } @@ -61,16 +67,34 @@ private WebGPUFlushContext( this.DeviceState = deviceState; } + /// + /// Gets the runtime lease that keeps the process-level WebGPU API alive. + /// public WebGPURuntime.Lease RuntimeLease { get; } + /// + /// Gets the WebGPU API facade for this flush. + /// public WebGPU Api { get; } + /// + /// Gets the device used to create and execute GPU resources. + /// public Device* Device { get; } + /// + /// Gets the queue used to submit GPU work. + /// public Queue* Queue { get; } + /// + /// Gets the target bounds for this flush context. + /// public Rectangle TargetBounds { get; } + /// + /// Gets the target texture format for this flush. + /// public TextureFormat TextureFormat { get; } /// @@ -78,12 +102,24 @@ private WebGPUFlushContext( /// public MemoryAllocator MemoryAllocator { get; } + /// + /// Gets device-scoped shared caches and reusable resources. + /// public DeviceSharedState DeviceState { get; } + /// + /// Gets the target texture receiving render/composite output. + /// public Texture* TargetTexture { get; private set; } + /// + /// Gets the texture view used when binding the target texture. + /// public TextureView* TargetView { get; private set; } + /// + /// Gets a value indicating whether CPU readback is required after GPU execution. + /// public bool RequiresReadback { get; private set; } /// @@ -91,22 +127,49 @@ private WebGPUFlushContext( /// public bool CanSampleTargetTexture { get; private set; } + /// + /// Gets the readback buffer used when CPU readback is required. + /// public WgpuBuffer* ReadbackBuffer { get; private set; } + /// + /// Gets the readback row stride in bytes. + /// public uint ReadbackBytesPerRow { get; private set; } + /// + /// Gets the readback buffer byte size. + /// public ulong ReadbackByteCount { get; private set; } + /// + /// Gets the shared instance-data buffer used for parameter uploads. + /// public WgpuBuffer* InstanceBuffer { get; private set; } + /// + /// Gets the instance buffer capacity in bytes. + /// public nuint InstanceBufferCapacity { get; private set; } + /// + /// Gets or sets the current write offset into . + /// public nuint InstanceBufferWriteOffset { get; internal set; } + /// + /// Gets or sets the active command encoder. + /// public CommandEncoder* CommandEncoder { get; set; } + /// + /// Gets the currently open render pass encoder, if any. + /// public RenderPassEncoder* PassEncoder { get; private set; } + /// + /// Creates a flush context for either a native WebGPU surface or a CPU-backed frame. + /// public static WebGPUFlushContext Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, @@ -160,6 +223,9 @@ public static WebGPUFlushContext Create( } } + /// + /// Creates a flush context intended for fallback upload into a writable native surface. + /// public static WebGPUFlushContext CreateUploadContext(ICanvasFrame frame, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { @@ -192,6 +258,9 @@ public static WebGPUFlushContext CreateUploadContext(ICanvasFrame + /// Rents a CPU fallback staging buffer for the specified pixel type and bounds. + /// public static FallbackStagingLease RentFallbackStaging(MemoryAllocator allocator, in Rectangle bounds) where TPixel : unmanaged, IPixel { @@ -202,6 +271,9 @@ public static FallbackStagingLease RentFallbackStaging(MemoryAll return ((FallbackStagingEntry)entry).Rent(allocator, in bounds); } + /// + /// Clears all cached CPU fallback staging buffers. + /// public static void ClearFallbackStagingCache() { foreach (IDisposable entry in FallbackStagingCache.Values) @@ -212,6 +284,9 @@ public static void ClearFallbackStagingCache() FallbackStagingCache.Clear(); } + /// + /// Clears all cached device-scoped shared state. + /// public static void ClearDeviceStateCache() { foreach (DeviceSharedState state in DeviceStateCache.Values) @@ -222,6 +297,13 @@ public static void ClearDeviceStateCache() DeviceStateCache.Clear(); } + /// + /// Tries to get shared native interop handles for the active WebGPU device and queue. + /// + /// When this method returns , contains the native device handle. + /// When this method returns , contains the native queue handle. + /// When this method returns , contains an error message. + /// if shared handles are available; otherwise . public static bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle, out string? error) { if (WebGPURuntime.TryGetSharedHandles(out Device* sharedDevice, out Queue* sharedQueue)) @@ -245,6 +327,12 @@ public static bool TryGetInteropHandles(out nint deviceHandle, out nint queueHan return false; } + /// + /// Ensures that the instance buffer exists and can hold at least the requested number of bytes. + /// + /// The required number of bytes for the current flush. + /// The minimum allocation size to enforce when creating a new buffer. + /// if the buffer is available with sufficient capacity; otherwise . public bool EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapacityBytes) { if (this.InstanceBuffer is not null && this.InstanceBufferCapacity >= requiredBytes) @@ -276,6 +364,10 @@ public bool EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapac return true; } + /// + /// Ensures that a command encoder is available for recording GPU commands. + /// + /// if an encoder is available; otherwise . public bool EnsureCommandEncoder() { if (this.CommandEncoder is not null) @@ -328,6 +420,9 @@ public bool BeginRenderPass(TextureView* targetView, bool loadExisting) return this.PassEncoder is not null; } + /// + /// Ends and releases the current render pass if one is active. + /// public void EndRenderPassIfOpen() { if (this.PassEncoder is null) @@ -340,6 +435,10 @@ public void EndRenderPassIfOpen() this.PassEncoder = null; } + /// + /// Tracks a transient bind group allocated during this flush. + /// + /// The bind group to track. public void TrackBindGroup(BindGroup* bindGroup) { if (bindGroup is not null) @@ -381,6 +480,12 @@ public void TrackTexture(Texture* texture) } } + /// + /// Tries to resolve a cached source texture view for an input image. + /// + /// The source image key. + /// When this method returns , contains the cached texture view. + /// if a cached texture view exists; otherwise . public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureView) { if (this.cachedSourceTextureViews.TryGetValue(image, out nint handle) && handle != 0) @@ -393,9 +498,17 @@ public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureV return false; } + /// + /// Caches a source texture view for reuse within the flush. + /// + /// The source image key. + /// The texture view to cache. public void CacheSourceTextureView(Image image, TextureView* textureView) => this.cachedSourceTextureViews[image] = (nint)textureView; + /// + /// Releases transient GPU resources owned by this flush context. + /// public void Dispose() { if (this.disposed) @@ -876,6 +989,9 @@ internal static void UploadTextureFromRegion( [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + /// + /// Shared device-scoped caches for pipelines, bind groups, and reusable GPU resources. + /// internal sealed class DeviceSharedState : IDisposable { private readonly ConcurrentDictionary cpuTargetCache = new(); @@ -896,12 +1012,29 @@ internal DeviceSharedState(WebGPU api, Device* device) private static ReadOnlySpan CompositeComputeEntryPoint => "cs_main\0"u8; + /// + /// Gets the synchronization object used for shared state mutation. + /// public object SyncRoot { get; } = new(); + /// + /// Gets the WebGPU API instance used by this shared state. + /// public WebGPU Api { get; } + /// + /// Gets the device associated with this shared state. + /// public Device* Device { get; } + /// + /// Rents CPU-target staging resources for a destination texture shape and format. + /// + /// The destination texture format. + /// The destination width. + /// The destination height. + /// The destination pixel size in bytes. + /// A lease for staging resources. public CpuTargetLease RentCpuTarget( TextureFormat textureFormat, int width, @@ -913,6 +1046,9 @@ public CpuTargetLease RentCpuTarget( return entry.Rent(this.Api, this.Device, in key); } + /// + /// Gets or creates a graphics pipeline used for composite rendering. + /// public bool TryGetOrCreateCompositePipeline( string pipelineKey, ReadOnlySpan shaderCode, @@ -997,6 +1133,9 @@ infrastructure.PipelineLayout is null || } } + /// + /// Gets or creates a compute pipeline used for composite execution. + /// public bool TryGetOrCreateCompositeComputePipeline( string pipelineKey, ReadOnlySpan shaderCode, @@ -1076,6 +1215,9 @@ infrastructure.PipelineLayout is null || } } + /// + /// Gets or creates a reusable shared buffer for device-scoped operations. + /// public bool TryGetOrCreateSharedBuffer( string bufferKey, BufferUsage usage, @@ -1150,6 +1292,9 @@ public bool TryGetOrCreateSharedBuffer( } } + /// + /// Releases all cached pipelines, buffers, and CPU-target entries owned by this state. + /// public void Dispose() { if (this.disposed) @@ -1426,31 +1571,56 @@ private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfra } } + /// + /// Cache key for CPU-target staging resources. + /// internal readonly struct CpuTargetCacheKey( TextureFormat textureFormat, int width, int height, int pixelSizeInBytes) : IEquatable { + /// + /// Gets the texture format for the cached CPU target. + /// public TextureFormat TextureFormat { get; } = textureFormat; + /// + /// Gets the target width. + /// public int Width { get; } = width; + /// + /// Gets the target height. + /// public int Height { get; } = height; + /// + /// Gets the pixel size in bytes. + /// public int PixelSizeInBytes { get; } = pixelSizeInBytes; + /// + /// Determines whether this key equals another CPU target cache key. + /// + /// The key to compare. + /// if all dimensions and format match; otherwise . public bool Equals(CpuTargetCacheKey other) => this.TextureFormat == other.TextureFormat && this.Width == other.Width && this.Height == other.Height && this.PixelSizeInBytes == other.PixelSizeInBytes; + /// public override bool Equals(object? obj) => obj is CpuTargetCacheKey other && this.Equals(other); + /// public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes); } + /// + /// Cache entry that owns the CPU-target staging resources. + /// internal sealed class CpuTargetEntry { private Texture* targetTexture; @@ -1460,6 +1630,9 @@ internal sealed class CpuTargetEntry private ulong readbackByteCount; private int inUse; + /// + /// Rents staging resources for the specified cache key. + /// internal CpuTargetLease Rent(WebGPU api, Device* device, in CpuTargetCacheKey key) { if (Interlocked.CompareExchange(ref this.inUse, 1, 0) == 0) @@ -1509,8 +1682,14 @@ internal CpuTargetLease Rent(WebGPU api, Device* device, in CpuTargetCacheKey ke temporaryReadbackByteCount); } + /// + /// Marks this entry as available for reuse. + /// internal void Release() => Volatile.Write(ref this.inUse, 0); + /// + /// Releases all resources currently owned by this entry. + /// internal void Dispose(WebGPU api) { ReleaseCpuTargetResources(api, this.targetTexture, this.targetView, this.readbackBuffer); @@ -1649,6 +1828,9 @@ private static void ReleaseCpuTargetResources( } } + /// + /// Lease wrapper for CPU-target staging resources. + /// public sealed class CpuTargetLease : IDisposable { private readonly WebGPU api; @@ -1676,16 +1858,34 @@ internal CpuTargetLease( this.ReadbackByteCount = readbackByteCount; } + /// + /// Gets the target texture used for CPU staging operations. + /// public Texture* TargetTexture { get; } + /// + /// Gets the texture view of . + /// public TextureView* TargetView { get; } + /// + /// Gets the readback buffer used to copy staged pixels to CPU memory. + /// public WgpuBuffer* ReadbackBuffer { get; } + /// + /// Gets the readback row stride in bytes. + /// public uint ReadbackBytesPerRow { get; } + /// + /// Gets the total readback buffer size in bytes. + /// public ulong ReadbackByteCount { get; } + /// + /// Releases leased resources or returns ownership to the shared cache entry. + /// public void Dispose() { if (Interlocked.Exchange(ref this.disposed, 1) != 0) @@ -1715,6 +1915,9 @@ public void Dispose() } } + /// + /// Shared render-pipeline infrastructure for compositing variants. + /// private sealed class CompositePipelineInfrastructure { public Dictionary<(TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs index 0c14037da..69d467770 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -3,10 +3,8 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Threading; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -19,6 +17,9 @@ internal static unsafe class WebGPUTestNativeSurfaceAllocator { private const int CallbackTimeoutMilliseconds = 5000; + /// + /// Tries to allocate a native WebGPU texture + view pair and wrap them in a . + /// internal static bool TryCreate( WebGPUDrawingBackend backend, int width, @@ -115,6 +116,9 @@ internal static bool TryCreate( return true; } + /// + /// Tries to upload CPU pixel data to an existing native WebGPU texture handle. + /// internal static bool TryWriteTexture( WebGPUDrawingBackend backend, nint textureHandle, @@ -162,6 +166,9 @@ internal static bool TryWriteTexture( } } + /// + /// Tries to read pixels from a native WebGPU texture handle into an . + /// internal static bool TryReadTexture( WebGPUDrawingBackend backend, nint textureHandle, @@ -329,6 +336,11 @@ void Callback(BufferMapAsyncStatus status, void* userData) } } + /// + /// Releases native texture and texture-view handles allocated for tests. + /// + /// The native texture handle. + /// The native texture-view handle. internal static void Release(nint textureHandle, nint textureViewHandle) { if (textureHandle == 0 && textureViewHandle == 0) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs index 07def310e..21d79f482 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs @@ -5,11 +5,24 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Maps public WebGPU texture format identifiers to Silk.NET texture formats and back. +/// internal static class WebGPUTextureFormatMapper { + /// + /// Converts a public WebGPU texture format identifier to the corresponding Silk.NET texture format. + /// + /// The public texture format identifier. + /// The matching value. public static TextureFormat ToSilk(WebGPUTextureFormatId formatId) => (TextureFormat)(int)formatId; + /// + /// Converts a Silk.NET texture format to the corresponding public WebGPU texture format identifier. + /// + /// The Silk.NET texture format. + /// The matching value. public static WebGPUTextureFormatId FromSilk(TextureFormat textureFormat) => (WebGPUTextureFormatId)(int)textureFormat; } From 3599c42b7672de8b0a1f5b766689ee33b1a10ee5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2026 22:23:08 +1000 Subject: [PATCH 031/136] Add ClearPath processors and refactor drawing API --- .../Backends/DefaultDrawingBackend.cs | 74 +++++--- .../Processing/DrawingCanvas{TPixel}.cs | 165 +++++++++--------- .../Processing/Extensions/ClearExtensions.cs | 5 +- .../Extensions/ClearPathExtensions.cs | 4 +- .../Processors/Drawing/ClearPathProcessor.cs | 46 +++++ .../Drawing/ClearPathProcessor{TPixel}.cs | 64 +++++++ .../Processors/Drawing/DrawPathProcessor.cs | 25 +-- .../Drawing/DrawPathProcessor{TPixel}.cs | 42 +++++ .../Processors/Drawing/FillPathProcessor.cs | 21 +-- .../Drawing/FillPathProcessor{TPixel}.cs | 2 +- .../Drawing/FillProcessor{TPixel}.cs | 2 +- .../Drawing/DrawTextRepeatedGlyphs.cs | 13 +- .../Drawing/FillPolygonTests.cs | 6 +- .../Backends/WebGPUDrawingBackendTests.cs | 50 ++++-- .../Processing/DrawingCanvasDrawImageTests.cs | 10 +- 15 files changed, 346 insertions(+), 183 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs create mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs create mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index c00146757..625e381a6 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -157,26 +158,9 @@ internal void FlushPreparedBatch( } // Iterate by row so we slice the already-rasterized coverage map once per command row. - for (int row = 0; row < maxHeight; row++) - { - for (int i = 0; i < commandCount; i++) - { - PreparedCompositionCommand command = commands[i]; - if (row >= command.DestinationRegion.Height) - { - continue; - } - - int destinationX = destinationBounds.X + command.DestinationRegion.X; - int destinationY = destinationBounds.Y + command.DestinationRegion.Y; - int sourceStartX = command.SourceOffset.X; - int sourceStartY = command.SourceOffset.Y; - - Span rowCoverage = coverageMap.DangerousGetRowSpan(sourceStartY + row); - Span rowSlice = rowCoverage.Slice(sourceStartX, command.DestinationRegion.Width); - applicators[i].Apply(rowSlice, destinationX, destinationY + row); - } - } + // We can do this in parallel since the applicators are thread-safe and each row is independent. + RowOperation operation = new(coverageMap, commands, applicators, destinationBounds, maxHeight); + ParallelRowIterator.IterateRows(configuration, destinationBounds, in operation); } finally { @@ -214,4 +198,54 @@ private Buffer2D CreateCoverageMap( return coverage; } + + private readonly struct RowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly Buffer2D coverageMap; + private readonly IReadOnlyList commands; + private readonly BrushApplicator[] applicators; + private readonly Rectangle destinationBounds; + private readonly int maxHeight; + + public RowOperation( + Buffer2D coverageMap, + IReadOnlyList commands, + BrushApplicator[] applicators, + Rectangle destinationBounds, + int maxHeight) + { + this.coverageMap = coverageMap; + this.commands = commands; + this.applicators = applicators; + this.destinationBounds = destinationBounds; + this.maxHeight = maxHeight; + } + + public void Invoke(int y) + { + if (y >= this.maxHeight) + { + return; + } + + for (int i = 0; i < this.commands.Count; i++) + { + PreparedCompositionCommand command = this.commands[i]; + if (y >= command.DestinationRegion.Height) + { + continue; + } + + int destinationX = this.destinationBounds.X + command.DestinationRegion.X; + int destinationY = this.destinationBounds.Y + command.DestinationRegion.Y; + int sourceStartX = command.SourceOffset.X; + int sourceStartY = command.SourceOffset.Y; + + Span rowCoverage = this.coverageMap.DangerousGetRowSpan(sourceStartY + y); + Span rowSlice = rowCoverage.Slice(sourceStartX, command.DestinationRegion.Width); + this.applicators[i].Apply(rowSlice, destinationX, destinationY + y); + } + } + } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 245a9d7b0..3aefd9ca6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -138,44 +138,50 @@ public DrawingCanvas CreateRegion(Rectangle region) return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher); } + /// + /// Clears the whole canvas using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + /// Drawing options used as the source for clear operation settings. + public void Clear(Brush brush, DrawingOptions options) + => this.Fill(brush, options.CloneForClearOperation()); + + /// + /// Clears a local region using the given brush and clear-style composition options. + /// + /// Region to clear in local coordinates. + /// Brush used to shade destination pixels during clear. + /// Drawing options used as the source for clear operation settings. + public void ClearRegion(Rectangle region, Brush brush, DrawingOptions options) + => this.FillRegion(region, brush, options.CloneForClearOperation()); + + /// + /// Clears a path region using the given brush and clear-style composition options. + /// + /// The path region to clear. + /// Brush used to shade destination pixels during clear. + /// Drawing options used as the source for clear operation settings. + public void ClearPath(IPath path, Brush brush, DrawingOptions options) + => this.FillPath(path, brush, options.CloneForClearOperation()); + /// /// Fills the whole canvas using the given brush. /// /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - public void Fill(Brush brush, GraphicsOptions graphicsOptions) - => this.FillRegion(this.Bounds, brush, graphicsOptions); + /// Drawing options for fill and rasterization behavior. + public void Fill(Brush brush, DrawingOptions options) + => this.FillRegion(this.Bounds, brush, options); /// /// Fills a local region using the given brush. /// /// Region to fill in local coordinates. /// Brush used to shade destination pixels. - /// Graphics blending/composition options. - public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOptions) + /// Drawing options for fill and rasterization behavior. + public void FillRegion(Rectangle region, Brush brush, DrawingOptions options) { this.EnsureNotDisposed(); - Guard.NotNull(brush, nameof(brush)); - Guard.NotNull(graphicsOptions, nameof(graphicsOptions)); - - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - RasterizerOptions rasterizerOptions = new( - region, - IntersectionRule.NonZero, - rasterizationMode, - RasterizerSamplingOrigin.PixelBoundary); - - RectangularPolygon regionPath = new(region.X, region.Y, region.Width, region.Height); - this.batcher.AddComposition( - CompositionCommand.Create( - regionPath, - brush, - graphicsOptions, - rasterizerOptions, - this.targetFrame.Bounds.Location)); + this.FillPath(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), brush, options); } /// @@ -185,56 +191,17 @@ public void FillRegion(Rectangle region, Brush brush, GraphicsOptions graphicsOp /// Brush used to shade covered pixels. /// Drawing options for fill and rasterization behavior. public void FillPath(IPath path, Brush brush, DrawingOptions options) - => this.FillPath(path, brush, options, RasterizerSamplingOrigin.PixelBoundary); - - /// - /// Fills a path in local coordinates using an explicit rasterizer sampling origin. - /// - /// The path to fill. - /// Brush used to shade covered pixels. - /// Drawing options for fill and rasterization behavior. - /// Sampling origin used by the rasterizer. - internal void FillPath( - IPath path, - Brush brush, - DrawingOptions options, - RasterizerSamplingOrigin samplingOrigin) { this.EnsureNotDisposed(); Guard.NotNull(path, nameof(path)); Guard.NotNull(brush, nameof(brush)); Guard.NotNull(options, nameof(options)); - GraphicsOptions graphicsOptions = options.GraphicsOptions; - ShapeOptions shapeOptions = options.ShapeOptions; - RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - - RectangleF bounds = path.Bounds; - if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) - { - // Keep rasterizer interest aligned with center-sampled scan conversion. - bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); - } - - Rectangle interest = Rectangle.FromLTRB( - (int)MathF.Floor(bounds.Left), - (int)MathF.Floor(bounds.Top), - (int)MathF.Ceiling(bounds.Right), - (int)MathF.Ceiling(bounds.Bottom)); - - RasterizerOptions rasterizerOptions = new( - interest, - shapeOptions.IntersectionRule, - rasterizationMode, - samplingOrigin); + IPath transformedPath = options.Transform == Matrix3x2.Identity + ? path + : path.Transform(options.Transform); - this.backend.FillPath( - this.targetFrame, - path, - brush, - graphicsOptions, - rasterizerOptions, - this.batcher); + this.FillPathCore(transformedPath, brush, options, RasterizerSamplingOrigin.PixelBoundary); } /// @@ -250,7 +217,10 @@ public void DrawPath(IPath path, Pen pen, DrawingOptions options) Guard.NotNull(pen, nameof(pen)); Guard.NotNull(options, nameof(options)); - IPath outline = pen.GeneratePath(path); + IPath transformedPath = options.Transform == Matrix3x2.Identity + ? path + : path.Transform(options.Transform); + IPath outline = pen.GeneratePath(transformedPath); DrawingOptions effectiveOptions = options; @@ -263,7 +233,7 @@ public void DrawPath(IPath path, Pen pen, DrawingOptions options) effectiveOptions = new DrawingOptions(options.GraphicsOptions, shapeOptions, options.Transform); } - this.FillPath(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); + this.FillPathCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); } /// @@ -412,6 +382,44 @@ public void DrawImage( } } + private void FillPathCore( + IPath path, + Brush brush, + DrawingOptions options, + RasterizerSamplingOrigin samplingOrigin) + { + GraphicsOptions graphicsOptions = options.GraphicsOptions; + ShapeOptions shapeOptions = options.ShapeOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + + RectangleF bounds = path.Bounds; + if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + // Keep rasterizer interest aligned with center-sampled scan conversion. + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + shapeOptions.IntersectionRule, + rasterizationMode, + samplingOrigin); + + this.backend.FillPath( + this.targetFrame, + path, + brush, + graphicsOptions, + rasterizerOptions, + this.batcher); + } + /// /// Converts rendered text operations to composition commands and submits them to the batcher. /// @@ -420,25 +428,22 @@ public void DrawImage( private void DrawTextOperations(List operations, DrawingOptions drawingOptions) { this.EnsureNotDisposed(); - Guard.NotNull(operations, nameof(operations)); - Guard.NotNull(drawingOptions, nameof(drawingOptions)); - // Build composition commands and sort by render pass then definition key so that - // same-coverage glyph variants are contiguous. Text glyphs within the same render - // pass occupy non-overlapping positions, making this reordering visually safe while - // maximizing batch sizes in the downstream batcher. + // Build composition commands and enforce render-pass ordering while preserving + // original emission order inside each pass. This preserves overlapping color-font + // layer compositing semantics (for example emoji mouth/teeth layers). Dictionary definitionKeyCache = []; - List<(byte RenderPass, CompositionCommand Command)> entries = new(operations.Count); + List<(byte RenderPass, int Sequence, CompositionCommand Command)> entries = new(operations.Count); for (int i = 0; i < operations.Count; i++) { DrawingOperation operation = operations[i]; - entries.Add((operation.RenderPass, this.CreateCompositionCommand(operation, drawingOptions, definitionKeyCache))); + entries.Add((operation.RenderPass, i, this.CreateCompositionCommand(operation, drawingOptions, definitionKeyCache))); } entries.Sort(static (a, b) => { int cmp = a.RenderPass.CompareTo(b.RenderPass); - return cmp != 0 ? cmp : a.Command.DefinitionKey.CompareTo(b.Command.DefinitionKey); + return cmp != 0 ? cmp : a.Sequence.CompareTo(b.Sequence); }); for (int i = 0; i < entries.Count; i++) diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs index f60e0f211..6813c8fca 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs @@ -44,7 +44,10 @@ public static IImageProcessingContext Clear(this IImageProcessingContext source, /// The brush. /// The to allow chaining of operations. public static IImageProcessingContext Clear(this IImageProcessingContext source, DrawingOptions options, Brush brush) - => source.Fill(options.CloneForClearOperation(), brush); + { + Size size = source.GetCurrentSize(); + return source.Clear(options, brush, new RectangularPolygon(0, 0, size.Width, size.Height)); + } /// /// Clones the path graphic options and applies changes required to force clearing. diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs index f5ea9a1f9..36505d90f 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -66,5 +68,5 @@ public static IImageProcessingContext Clear( DrawingOptions options, Brush brush, IPath region) - => source.Fill(options.CloneForClearOperation(), brush, region); + => source.ApplyProcessor(new ClearPathProcessor(options, brush, region)); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs new file mode 100644 index 000000000..900e4b29f --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +/// +/// Defines a processor to clear pixels within a given +/// with the given using clear composition semantics defined by . +/// +public class ClearPathProcessor : IImageProcessor +{ + /// + /// Initializes a new instance of the class. + /// + /// The drawing options. + /// The details how to clear the region of interest. + /// The logic path to be cleared. + public ClearPathProcessor(DrawingOptions options, Brush brush, IPath path) + { + this.Region = path; + this.Brush = brush; + this.Options = options; + } + + /// + /// Gets the used for clearing the destination image. + /// + public Brush Brush { get; } + + /// + /// Gets the logic path that this processor applies to. + /// + public IPath Region { get; } + + /// + /// Gets the defining clear composition behavior. + /// + public DrawingOptions Options { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new ClearPathProcessor(configuration, this, source, sourceRectangle); +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs new file mode 100644 index 000000000..487076ba9 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +/// +/// Uses a brush and a shape to clear the shape with clear composition semantics. +/// +/// The type of the color. +/// +internal class ClearPathProcessor : ImageProcessor + where TPixel : unmanaged, IPixel +{ + private readonly ClearPathProcessor definition; + private readonly IPath path; + private readonly Rectangle bounds; + + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The processor definition. + /// The source image. + /// The source bounds. + public ClearPathProcessor( + Configuration configuration, + ClearPathProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + IPath path = definition.Region; + int left = (int)MathF.Floor(path.Bounds.Left); + int top = (int)MathF.Floor(path.Bounds.Top); + int right = (int)MathF.Ceiling(path.Bounds.Right); + int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); + + this.bounds = Rectangle.FromLTRB(left, top, right, bottom); + this.path = path.AsClosedPath(); + this.definition = definition; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + Configuration configuration = this.Configuration; + Brush brush = this.definition.Brush; + + Rectangle interest = Rectangle.Intersect(this.bounds, source.Bounds); + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + using DrawingCanvas canvas = new( + configuration, + new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + + canvas.ClearPath(this.path, brush, this.definition.Options); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs index 3efbf1617..f4335778c 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; @@ -43,27 +42,5 @@ public DrawPathProcessor(DrawingOptions options, Pen pen, IPath path) /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - { - IPath outline = this.Pen.GeneratePath(this.Path); - - DrawingOptions effectiveOptions = this.Options; - - // Non-normalized stroked output can contain overlaps/self-intersections. - // Rasterizing these contours with non-zero winding matches the intended stroke semantics. - if (!this.Pen.StrokeOptions.NormalizeOutput && - this.Options.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) - { - ShapeOptions shapeOptions = this.Options.ShapeOptions.DeepClone(); - shapeOptions.IntersectionRule = IntersectionRule.NonZero; - - effectiveOptions = new DrawingOptions(this.Options.GraphicsOptions, shapeOptions, this.Options.Transform); - } - - return new FillPathProcessor( - effectiveOptions, - this.Pen.StrokeFill, - outline, - RasterizerSamplingOrigin.PixelCenter) - .CreatePixelSpecificProcessor(configuration, source, sourceRectangle); - } + => new DrawPathProcessor(configuration, this, source, sourceRectangle); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs new file mode 100644 index 000000000..6549a325e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +/// +/// Uses a pen and path to draw an outlined path through . +/// +/// The pixel format. +internal class DrawPathProcessor : ImageProcessor + where TPixel : unmanaged, IPixel +{ + private readonly DrawPathProcessor definition; + + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The processor definition. + /// The source image. + /// The source bounds. + public DrawPathProcessor( + Configuration configuration, + DrawPathProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + => this.definition = definition; + + /// + protected override void OnFrameApply(ImageFrame source) + { + using DrawingCanvas canvas = new( + this.Configuration, + new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + + canvas.DrawPath(this.definition.Path, this.definition.Pen, this.definition.Options); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 8b780fe9d..01065c76b 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -55,24 +55,5 @@ internal FillPathProcessor( /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - { - IPath shape = this.Region.Transform(this.Options.Transform); - - if (this.SamplingOrigin == RasterizerSamplingOrigin.PixelBoundary && - shape is RectangularPolygon rectPoly) - { - RectangleF rectF = new(rectPoly.Location, rectPoly.Size); - Rectangle rect = (Rectangle)rectF; - if (!this.Options.GraphicsOptions.Antialias || rectF == rect) - { - // Cast as in and back are the same or we are using anti-aliasing - return new FillProcessor(this.Options, this.Brush) - .CreatePixelSpecificProcessor(configuration, source, rect); - } - } - - // Clone the definition so we can pass the transformed path. - FillPathProcessor definition = new(this.Options, this.Brush, shape, this.SamplingOrigin); - return new FillPathProcessor(configuration, definition, source, sourceRectangle); - } + => new FillPathProcessor(configuration, this, source, sourceRectangle); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 62541f743..e6b337a23 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -60,6 +60,6 @@ protected override void OnFrameApply(ImageFrame source) configuration, new Buffer2DRegion(source.PixelBuffer, source.Bounds)); - canvas.FillPath(this.path, brush, this.definition.Options, this.definition.SamplingOrigin); + canvas.FillPath(this.path, brush, this.definition.Options); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 0fe36075a..4e74e002c 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -32,6 +32,6 @@ protected override void OnFrameApply(ImageFrame source) this.Configuration, new Buffer2DRegion(source.PixelBuffer, interest)); - canvas.Fill(this.definition.Brush, this.definition.Options.GraphicsOptions); + canvas.Fill(this.definition.Brush, this.definition.Options); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 3f1ed1934..d295d072e 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -26,12 +26,15 @@ public class DrawTextRepeatedGlyphs } }; - private readonly GraphicsOptions clearOptions = new() + private readonly DrawingOptions clearOptions = new() { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } }; private readonly Brush brush = Brushes.Solid(Color.HotPink); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index 3fc6f89fb..5739567e2 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -90,8 +90,10 @@ public void Fill_RectangularPolygon_Solid_TransformedUsingConfiguration( where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(25, 25, 50, 50); - provider.Configuration.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))); - provider.RunValidatingProcessorTest(c => c.Fill(Color.White, polygon)); + provider.RunValidatingProcessorTest( + c => c + .SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) + .Fill(Color.White, polygon)); } public static TheoryData FillPolygon_Complex_Data { get; } = diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 62d60e06f..cf9ae7541 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -92,11 +92,14 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid GraphicsOptions = new GraphicsOptions { Antialias = true } }; - GraphicsOptions clearOptions = new() + DrawingOptions clearOptions = new() { - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F + GraphicsOptions = new GraphicsOptions + { + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } }; RectangularPolygon polygon = new(36.5F, 26.25F, 312.5F, 188.5F); @@ -407,12 +410,15 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu { GraphicsOptions = new GraphicsOptions { Antialias = true } }; - GraphicsOptions clearOptions = new() + DrawingOptions clearOptions = new() { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } }; RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); @@ -457,12 +463,15 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef { GraphicsOptions = new GraphicsOptions { Antialias = true } }; - GraphicsOptions clearOptions = new() + DrawingOptions clearOptions = new() { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } }; Rectangle region = new(72, 64, 320, 240); @@ -574,12 +583,15 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes GraphicsOptions = new GraphicsOptions { Antialias = true } }; - GraphicsOptions clearOptions = new() + DrawingOptions clearOptions = new() { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } }; const int glyphCount = 200; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs index 6a3b6f09b..57150ab8d 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs @@ -24,20 +24,12 @@ public void DrawImage_WithRotationTransform_MatchesReference(TestImagePr provider.Configuration, new Buffer2DRegion(target.Frames.RootFrame.PixelBuffer, target.Bounds)); - GraphicsOptions clearOptions = new() - { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - }; - DrawingOptions options = new() { Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) }; - canvas.Fill(Brushes.Solid(Color.White), clearOptions); + canvas.Clear(Brushes.Solid(Color.White), options); canvas.DrawImage( foreground, foreground.Bounds, From db204ba63f53dc722ca075105c1aa01e6abaea00 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2026 22:30:23 +1000 Subject: [PATCH 032/136] Update ImageSharp.Drawing.csproj --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 338b613d1..e65da8e05 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -49,9 +49,9 @@ - - - + + + From d79398318db42004b8f55f8d6ef7c063705e8761 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 1 Mar 2026 22:35:27 +1000 Subject: [PATCH 033/136] Close path before transform; update processors --- src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs | 6 ++++-- .../Processors/Drawing/ClearPathProcessor{TPixel}.cs | 2 +- .../Processors/Drawing/FillPathProcessor{TPixel}.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 3aefd9ca6..cf2b2a4d1 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -197,9 +197,11 @@ public void FillPath(IPath path, Brush brush, DrawingOptions options) Guard.NotNull(brush, nameof(brush)); Guard.NotNull(options, nameof(options)); + IPath closed = path.AsClosedPath(); + IPath transformedPath = options.Transform == Matrix3x2.Identity - ? path - : path.Transform(options.Transform); + ? closed + : closed.Transform(options.Transform); this.FillPathCore(transformedPath, brush, options, RasterizerSamplingOrigin.PixelBoundary); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs index 487076ba9..a12372236 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs @@ -39,7 +39,7 @@ public ClearPathProcessor( int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); this.bounds = Rectangle.FromLTRB(left, top, right, bottom); - this.path = path.AsClosedPath(); + this.path = path; this.definition = definition; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index e6b337a23..2642a1d98 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -39,7 +39,7 @@ public FillPathProcessor( int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); this.bounds = Rectangle.FromLTRB(left, top, right, bottom); - this.path = path.AsClosedPath(); + this.path = path; this.definition = definition; } From 5cb9b2dd81f4962b70c90d67bd14c90e260fa7c9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 2 Mar 2026 22:30:28 +1000 Subject: [PATCH 034/136] Add DrawingCanvasState and stateful DrawingCanvas API --- .../Processing/DrawingCanvasState.cs | 51 +++ .../Processing/DrawingCanvas{TPixel}.cs | 308 ++++++++++++++---- .../Drawing/ClearPathProcessor{TPixel}.cs | 5 +- .../Drawing/DrawPathProcessor{TPixel}.cs | 5 +- .../Drawing/FillPathProcessor{TPixel}.cs | 5 +- .../Drawing/FillProcessor{TPixel}.cs | 5 +- .../Text/DrawTextProcessor{TPixel}.cs | 4 +- .../Shapes/ArcLineSegment.cs | 7 +- .../Drawing/DrawTextRepeatedGlyphs.cs | 16 +- .../Backends/WebGPUDrawingBackendTests.cs | 137 ++++---- .../Processing/DrawingCanvasBatcherTests.cs | 14 +- .../Processing/DrawingCanvasDrawImageTests.cs | 12 +- 12 files changed, 397 insertions(+), 172 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs new file mode 100644 index 000000000..c6a4e5f38 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Immutable drawing state used by . +/// +public sealed class DrawingCanvasState : IDisposable +{ + private readonly Action? releaseScopedState; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Drawing options for this state. + /// Clip paths for this state. + /// Optional callback invoked when a scoped state is disposed. + internal DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths, Action? releaseScopedState = null) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + this.Options = options; + this.ClipPaths = clipPaths; + this.releaseScopedState = releaseScopedState; + } + + /// + /// Gets drawing options associated with this state. + /// + public DrawingOptions Options { get; } + + /// + /// Gets clip paths associated with this state. + /// + public IReadOnlyList ClipPaths { get; } + + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.releaseScopedState?.Invoke(); + this.disposed = true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index cf2b2a4d1..e58f5d662 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -44,6 +44,11 @@ public sealed class DrawingCanvas : IDisposable /// private readonly List> pendingImageResources = []; + /// + /// Represents the default state configuration for the drawing canvas. + /// + private readonly DrawingCanvasState defaultState; + /// /// Tracks whether this instance has already been disposed. /// @@ -54,8 +59,14 @@ public sealed class DrawingCanvas : IDisposable /// /// The active processing configuration. /// The destination target region. - public DrawingCanvas(Configuration configuration, Buffer2DRegion targetRegion) - : this(configuration, new CpuCanvasFrame(targetRegion)) + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + Buffer2DRegion targetRegion, + DrawingOptions options, + params IPath[] clipPaths) + : this(configuration, new CpuCanvasFrame(targetRegion), options, clipPaths) { } @@ -64,27 +75,37 @@ public DrawingCanvas(Configuration configuration, Buffer2DRegion targetR /// /// The active processing configuration. /// The destination frame. - public DrawingCanvas(Configuration configuration, ICanvasFrame targetFrame) - : this(configuration, configuration.GetDrawingBackend(), targetFrame) + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + ICanvasFrame targetFrame, + DrawingOptions options, + params IPath[] clipPaths) + : this(configuration, configuration.GetDrawingBackend(), targetFrame, options, clipPaths) { } /// - /// Initializes a new instance of the class - /// with an explicit backend. + /// Initializes a new instance of the class with an explicit backend and initial state. /// /// The active processing configuration. /// The drawing backend implementation. /// The destination frame. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. internal DrawingCanvas( Configuration configuration, IDrawingBackend backend, - ICanvasFrame targetFrame) + ICanvasFrame targetFrame, + DrawingOptions options, + params IPath[] clipPaths) : this( configuration, backend, targetFrame, - new DrawingCanvasBatcher(configuration, backend, targetFrame)) + new DrawingCanvasBatcher(configuration, backend, targetFrame), + new DrawingCanvasState(options, clipPaths)) { } @@ -96,16 +117,19 @@ internal DrawingCanvas( /// The drawing backend implementation. /// The destination frame. /// The command batcher used for deferred composition. + /// The default state used when no scoped state is active. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, - DrawingCanvasBatcher batcher) + DrawingCanvasBatcher batcher, + DrawingCanvasState defaultState) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); Guard.NotNull(targetFrame, nameof(targetFrame)); Guard.NotNull(batcher, nameof(batcher)); + Guard.NotNull(defaultState, nameof(defaultState)); if (!targetFrame.TryGetCpuRegion(out _) && !targetFrame.TryGetNativeSurface(out _)) { @@ -117,6 +141,7 @@ private DrawingCanvas( this.targetFrame = targetFrame; this.batcher = batcher; this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); + this.defaultState = defaultState; } /// @@ -124,6 +149,12 @@ private DrawingCanvas( /// public Rectangle Bounds { get; } + /// + /// Gets or sets the current state of the drawing canvas within the current scope. + /// The value may be if no state is set. + /// + internal DrawingCanvasState? ScopedState { get; set; } + /// /// Creates a child canvas over a subregion in local coordinates. /// @@ -135,53 +166,88 @@ public DrawingCanvas CreateRegion(Rectangle region) Rectangle clipped = Rectangle.Intersect(this.Bounds, region); ICanvasFrame childFrame = new CanvasRegionFrame(this.targetFrame, clipped); - return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher); + return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.defaultState); + } + + /// + /// Creates an immutable scoped drawing state and applies it to this canvas until disposed. + /// + /// Drawing options for the scoped state. + /// Clip paths associated with the scoped state. + /// The active scoped state that restores to default when disposed. + public DrawingCanvasState CreateState(DrawingOptions options, params IPath[] clipPaths) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + DrawingCanvasState state = new(options, clipPaths, () => this.ScopedState = null); + this.ScopedState = state; + return state; } /// /// Clears the whole canvas using the given brush and clear-style composition options. /// /// Brush used to shade destination pixels during clear. - /// Drawing options used as the source for clear operation settings. - public void Clear(Brush brush, DrawingOptions options) - => this.Fill(brush, options.CloneForClearOperation()); + public void Clear(Brush brush) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush)); + } /// /// Clears a local region using the given brush and clear-style composition options. /// /// Region to clear in local coordinates. /// Brush used to shade destination pixels during clear. - /// Drawing options used as the source for clear operation settings. - public void ClearRegion(Rectangle region, Brush brush, DrawingOptions options) - => this.FillRegion(region, brush, options.CloneForClearOperation()); + public void Clear(Rectangle region, Brush brush) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(region, brush)); + } /// /// Clears a path region using the given brush and clear-style composition options. /// /// The path region to clear. /// Brush used to shade destination pixels during clear. - /// Drawing options used as the source for clear operation settings. - public void ClearPath(IPath path, Brush brush, DrawingOptions options) - => this.FillPath(path, brush, options.CloneForClearOperation()); + public void Clear(IPath path, Brush brush) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(path, brush)); + } /// /// Fills the whole canvas using the given brush. /// /// Brush used to shade destination pixels. - /// Drawing options for fill and rasterization behavior. - public void Fill(Brush brush, DrawingOptions options) - => this.FillRegion(this.Bounds, brush, options); + public void Fill(Brush brush) + => this.Fill(this.Bounds, brush); /// /// Fills a local region using the given brush. /// /// Region to fill in local coordinates. /// Brush used to shade destination pixels. - /// Drawing options for fill and rasterization behavior. - public void FillRegion(Rectangle region, Brush brush, DrawingOptions options) + public void Fill(Rectangle region, Brush brush) + => this.Fill(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), brush); + + /// + /// Fills all paths in a collection using the given brush and drawing options. + /// + /// Brush used to shade covered pixels. + /// Path collection to fill. + public void Fill(Brush brush, IPathCollection paths) { - this.EnsureNotDisposed(); - this.FillPath(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), brush, options); + Guard.NotNull(paths, nameof(paths)); + foreach (IPath path in paths) + { + this.Fill(path, brush); + } } /// @@ -189,52 +255,122 @@ public void FillRegion(Rectangle region, Brush brush, DrawingOptions options) /// /// The path to fill. /// Brush used to shade covered pixels. - /// Drawing options for fill and rasterization behavior. - public void FillPath(IPath path, Brush brush, DrawingOptions options) + public void Fill(IPath path, Brush brush) { this.EnsureNotDisposed(); Guard.NotNull(path, nameof(path)); Guard.NotNull(brush, nameof(brush)); - Guard.NotNull(options, nameof(options)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; IPath closed = path.AsClosedPath(); - IPath transformedPath = options.Transform == Matrix3x2.Identity + IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity ? closed - : closed.Transform(options.Transform); + : closed.Transform(effectiveOptions.Transform); + + transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); + + this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); + } + + /// + /// Draws an arc outline using the provided pen and drawing options. + /// + /// Pen used to generate the arc outline. + /// Arc center point in local coordinates. + /// Arc radii in local coordinates. + /// Ellipse rotation in degrees. + /// Arc start angle in degrees. + /// Arc sweep angle in degrees. + public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); + + /// + /// Draws a cubic bezier outline using the provided pen and drawing options. + /// + /// Pen used to generate the bezier outline. + /// Bezier control points. + public void DrawBezier(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + this.Draw(pen, new Path(new CubicBezierLineSegment(points))); + } + + /// + /// Draws an ellipse outline using the provided pen and drawing options. + /// + /// Pen used to generate the ellipse outline. + /// Ellipse center point in local coordinates. + /// Ellipse width and height in local coordinates. + public void DrawEllipse(Pen pen, PointF center, SizeF size) + => this.Draw(pen, new EllipsePolygon(center, size)); - this.FillPathCore(transformedPath, brush, options, RasterizerSamplingOrigin.PixelBoundary); + /// + /// Draws a polyline outline using the provided pen and drawing options. + /// + /// Pen used to generate the line outline. + /// Polyline points. + public void DrawLine(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + this.Draw(pen, new Path(points)); + } + + /// + /// Draws a rectangular outline using the provided pen and drawing options. + /// + /// Pen used to generate the rectangle outline. + /// Rectangle region to stroke. + public void Draw(Pen pen, Rectangle region) + => this.Draw(pen, new RectangularPolygon(region.X, region.Y, region.Width, region.Height)); + + /// + /// Draws all paths in a collection using the provided pen and drawing options. + /// + /// Pen used to generate outlines. + /// Path collection to stroke. + public void Draw(Pen pen, IPathCollection paths) + { + Guard.NotNull(paths, nameof(paths)); + foreach (IPath path in paths) + { + this.Draw(pen, path); + } } /// /// Draws a path outline in local coordinates using the given pen. /// - /// The path to stroke. /// Pen used to generate the outline fill path. - /// Drawing options for stroke fill and rasterization behavior. - public void DrawPath(IPath path, Pen pen, DrawingOptions options) + /// The path to stroke. + public void Draw(Pen pen, IPath path) { this.EnsureNotDisposed(); - Guard.NotNull(path, nameof(path)); Guard.NotNull(pen, nameof(pen)); - Guard.NotNull(options, nameof(options)); + Guard.NotNull(path, nameof(path)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; - IPath transformedPath = options.Transform == Matrix3x2.Identity + IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity ? path - : path.Transform(options.Transform); - IPath outline = pen.GeneratePath(transformedPath); + : path.Transform(effectiveOptions.Transform); - DrawingOptions effectiveOptions = options; + IPath outline = pen.GeneratePath(transformedPath); // Non-normalized stroke output can self-overlap; non-zero winding preserves stroke semantics. if (!pen.StrokeOptions.NormalizeOutput && - options.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) { - ShapeOptions shapeOptions = options.ShapeOptions.DeepClone(); + ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); shapeOptions.IntersectionRule = IntersectionRule.NonZero; - effectiveOptions = new DrawingOptions(options.GraphicsOptions, shapeOptions, options.Transform); + effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); } + outline = ApplyClipPaths(outline, effectiveOptions.ShapeOptions, state.ClipPaths); + this.FillPathCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); } @@ -243,20 +379,20 @@ public void DrawPath(IPath path, Pen pen, DrawingOptions options) /// /// The text rendering options. /// The text to draw. - /// Drawing options defining blending and shape behavior. /// Optional brush used to fill glyphs. /// Optional pen used to outline glyphs. public void DrawText( RichTextOptions textOptions, string text, - DrawingOptions drawingOptions, Brush? brush, Pen? pen) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); Guard.NotNull(text, nameof(text)); - Guard.NotNull(drawingOptions, nameof(drawingOptions)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; if (brush is null && pen is null) { @@ -264,11 +400,11 @@ public void DrawText( } RichTextOptions configuredOptions = ConfigureTextOptions(textOptions); - using RichTextGlyphRenderer textRenderer = new(configuredOptions, drawingOptions, pen, brush); - TextRenderer renderer = new(textRenderer); + using RichTextGlyphRenderer glyphRenderer = new(configuredOptions, effectiveOptions, pen, brush); + TextRenderer renderer = new(glyphRenderer); renderer.RenderText(text, configuredOptions); - this.DrawTextOperations(textRenderer.DrawingOperations, drawingOptions); + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); } /// @@ -277,7 +413,6 @@ public void DrawText( /// The source image. /// The source rectangle within . /// The destination rectangle in local canvas coordinates. - /// Drawing options defining blend and transform behavior. /// /// Optional resampler used when scaling or transforming the image. Defaults to . /// @@ -285,12 +420,13 @@ public void DrawImage( Image image, Rectangle sourceRect, RectangleF destinationRect, - DrawingOptions drawingOptions, IResampler? sampler = null) { this.EnsureNotDisposed(); Guard.NotNull(image, nameof(image)); - Guard.NotNull(drawingOptions, nameof(drawingOptions)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; if (sourceRect.Width <= 0 || sourceRect.Height <= 0 || @@ -342,12 +478,12 @@ public void DrawImage( } // Phase 2: Apply canvas transform to image content when requested. - if (drawingOptions.Transform != Matrix3x2.Identity) + if (effectiveOptions.Transform != Matrix3x2.Identity) { Image transformed = CreateTransformedDrawImage( brushImage, clippedDestinationRect, - drawingOptions.Transform, + effectiveOptions.Transform, sampler, out renderDestinationRect); @@ -376,7 +512,7 @@ public void DrawImage( renderDestinationRect.Width, renderDestinationRect.Height); - this.FillPath(destinationPath, brush, drawingOptions); + this.Fill(destinationPath, brush); } finally { @@ -384,6 +520,13 @@ public void DrawImage( } } + /// + /// Rasterizes and submits a fill operation to the backend. + /// + /// Path to fill. + /// Brush used for shading. + /// Effective drawing options. + /// Rasterizer sampling origin. private void FillPathCore( IPath path, Brush brush, @@ -427,7 +570,11 @@ private void FillPathCore( /// /// Text drawing operations produced by glyph layout/rendering. /// Drawing options applied to each operation. - private void DrawTextOperations(List operations, DrawingOptions drawingOptions) + /// Clip paths resolved from effective canvas state. + private void DrawTextOperations( + List operations, + DrawingOptions drawingOptions, + IReadOnlyList clipPaths) { this.EnsureNotDisposed(); @@ -439,7 +586,9 @@ private void DrawTextOperations(List operations, DrawingOption for (int i = 0; i < operations.Count; i++) { DrawingOperation operation = operations[i]; - entries.Add((operation.RenderPass, i, this.CreateCompositionCommand(operation, drawingOptions, definitionKeyCache))); + DrawingOperation clippedOperation = operation; + clippedOperation.Path = ApplyClipPaths(operation.Path, drawingOptions.ShapeOptions, clipPaths); + entries.Add((operation.RenderPass, i, this.CreateCompositionCommand(clippedOperation, drawingOptions, definitionKeyCache))); } entries.Sort(static (a, b) => @@ -454,6 +603,49 @@ private void DrawTextOperations(List operations, DrawingOption } } + /// + /// Resolves the currently active drawing state. + /// + /// The scoped state when present; otherwise the default state. + private DrawingCanvasState ResolveState() => this.ScopedState ?? this.defaultState; + + /// + /// Executes an action with a temporary scoped state, restoring the previous scoped state afterwards. + /// + /// Temporary drawing options. + /// Temporary clip paths. + /// Action to execute. + private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) + { + DrawingCanvasState? previous = this.ScopedState; + this.ScopedState = new DrawingCanvasState(options, clipPaths); + try + { + action(); + } + finally + { + this.ScopedState = previous; + } + } + + /// + /// Applies all clip paths to a subject path using the provided shape options. + /// + /// Path to clip. + /// Shape options used for clipping. + /// Clip paths to apply. + /// The clipped path. + private static IPath ApplyClipPaths(IPath subjectPath, ShapeOptions shapeOptions, IReadOnlyList clipPaths) + { + if (clipPaths.Count == 0) + { + return subjectPath; + } + + return subjectPath.Clip(shapeOptions, clipPaths); + } + /// /// Flushes queued drawing commands to the target in submission order. /// diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs index a12372236..c942b2fe7 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs @@ -57,8 +57,9 @@ protected override void OnFrameApply(ImageFrame source) using DrawingCanvas canvas = new( configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + new Buffer2DRegion(source.PixelBuffer, source.Bounds), + this.definition.Options); - canvas.ClearPath(this.path, brush, this.definition.Options); + canvas.Clear(this.path, brush); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs index 6549a325e..ec22ed8f3 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs @@ -35,8 +35,9 @@ protected override void OnFrameApply(ImageFrame source) { using DrawingCanvas canvas = new( this.Configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + new Buffer2DRegion(source.PixelBuffer, source.Bounds), + this.definition.Options); - canvas.DrawPath(this.definition.Path, this.definition.Pen, this.definition.Options); + canvas.Draw(this.definition.Pen, this.definition.Path); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 2642a1d98..d44a46fcb 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -58,8 +58,9 @@ protected override void OnFrameApply(ImageFrame source) using DrawingCanvas canvas = new( configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + new Buffer2DRegion(source.PixelBuffer, source.Bounds), + this.definition.Options); - canvas.FillPath(this.path, brush, this.definition.Options); + canvas.Fill(this.path, brush); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 4e74e002c..5bfe77c8a 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -30,8 +30,9 @@ protected override void OnFrameApply(ImageFrame source) using DrawingCanvas canvas = new( this.Configuration, - new Buffer2DRegion(source.PixelBuffer, interest)); + new Buffer2DRegion(source.PixelBuffer, interest), + this.definition.Options); - canvas.Fill(this.definition.Brush, this.definition.Options); + canvas.Fill(this.definition.Brush); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index a663a7c1d..23dce9646 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -24,12 +24,12 @@ protected override void OnFrameApply(ImageFrame source) { using DrawingCanvas canvas = new( this.Configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds)); + new Buffer2DRegion(source.PixelBuffer, source.Bounds), + this.definition.DrawingOptions); canvas.DrawText( this.definition.TextOptions, this.definition.Text, - this.definition.DrawingOptions, this.definition.Brush, this.definition.Pen); } diff --git a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs b/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs index 994753da2..b897d1d9b 100644 --- a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs +++ b/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs @@ -80,10 +80,7 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn } } - private ArcLineSegment(PointF[] linePoints) - { - this.linePoints = linePoints; - } + private ArcLineSegment(PointF[] linePoints) => this.linePoints = linePoints; /// public PointF EndPoint => this.linePoints[^1]; @@ -203,7 +200,7 @@ private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, V prev = p2; } - return points.ToArray(); + return [.. points]; } private static void EndpointToCenterArcParams( diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index d295d072e..219c9eb96 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -118,8 +118,8 @@ public void DrawingCanvasDefaultBackend() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.defaultImage)); // this.ClearWithDrawingCanvas(this.defaultConfiguration, frame); - using DrawingCanvas canvas = new(this.defaultConfiguration, frame); - canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + using DrawingCanvas canvas = new(this.defaultConfiguration, frame, this.drawingOptions); + canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); } @@ -128,8 +128,8 @@ public void DrawingCanvasWebGPUBackendCpuRegion() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); // this.ClearWithDrawingCanvas(this.webGpuConfiguration, frame); - using DrawingCanvas canvas = new(this.webGpuConfiguration, frame); - canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + using DrawingCanvas canvas = new(this.webGpuConfiguration, frame, this.drawingOptions); + canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); } @@ -137,15 +137,15 @@ public void DrawingCanvasWebGPUBackendCpuRegion() public void DrawingCanvasWebGPUBackendNativeSurface() { // this.ClearWithDrawingCanvas(this.webGpuConfiguration, this.webGpuNativeFrame); - using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame); - canvas.DrawText(this.textOptions, this.text, this.drawingOptions, this.brush, pen: null); + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, this.drawingOptions); + canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); } private void ClearWithDrawingCanvas(Configuration configuration, ICanvasFrame target) { - using DrawingCanvas canvas = new(configuration, target); - canvas.Fill(this.clearBrush, this.clearOptions); + using DrawingCanvas canvas = new(configuration, target, this.clearOptions); + canvas.Fill(this.clearBrush); canvas.Flush(); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index cf9ae7541..396fac3c0 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -42,14 +42,14 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(polygon, brush); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -57,6 +57,7 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -92,16 +93,6 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid GraphicsOptions = new GraphicsOptions { Antialias = true } }; - DrawingOptions clearOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - } - }; - RectangularPolygon polygon = new(36.5F, 26.25F, 312.5F, 188.5F); Brush clearBrush = Brushes.Solid(Color.White); @@ -109,22 +100,23 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); void DrawAction(DrawingCanvas canvas) { - canvas.Fill(clearBrush, clearOptions); - canvas.FillPath(polygon, brush, drawingOptions); + canvas.Clear(clearBrush); + canvas.Fill(polygon, brush); } using Image defaultImage = new(384, 256); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = new(384, 256); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, (Action>)DrawAction); DebugSaveBackendTriplet(provider, "FillPath_ImageBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); @@ -191,14 +183,14 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.FillPath(path, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(path, brush); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -206,6 +198,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -253,21 +246,22 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput canvas) => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(polygon, brush); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = baseImage.Clone(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, baseImage); @@ -308,21 +302,22 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); - void DrawAction(DrawingCanvas canvas) => canvas.FillPath(polygon, brush, drawingOptions); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(polygon, brush); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = baseImage.Clone(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, baseImage); @@ -359,15 +354,14 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag string text = "Sphinx of black quartz, judge my vow\n0123456789"; Brush brush = Brushes.Solid(Color.Black); Pen pen = Pens.Solid(Color.OrangeRed, 2F); - void DrawAction(DrawingCanvas canvas) => - canvas.DrawText(textOptions, text, drawingOptions, brush, pen); + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, pen); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -375,6 +369,7 @@ void DrawAction(DrawingCanvas canvas) => defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -410,32 +405,22 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu { GraphicsOptions = new GraphicsOptions { Antialias = true } }; - DrawingOptions clearOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - } - }; RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); void DrawAction(DrawingCanvas canvas) { - canvas.Fill(clearBrush, clearOptions); - canvas.FillPath(polygon, brush, drawingOptions); + canvas.Clear(clearBrush); + canvas.Fill(polygon, brush); } using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -443,6 +428,7 @@ void DrawAction(DrawingCanvas canvas) defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -463,34 +449,24 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDef { GraphicsOptions = new GraphicsOptions { Antialias = true } }; - DrawingOptions clearOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - } - }; - Rectangle region = new(72, 64, 320, 240); RectangularPolygon localPolygon = new(16.25F, 24.5F, 250.5F, 160.75F); Brush brush = Brushes.Solid(Color.Black); Brush clearBrush = Brushes.Solid(Color.White); void DrawAction(DrawingCanvas canvas) { - canvas.Fill(clearBrush, clearOptions); + canvas.Clear(clearBrush); + using DrawingCanvas regionCanvas = canvas.CreateRegion(region); - regionCanvas.FillPath(localPolygon, brush, drawingOptions); + regionCanvas.Fill(localPolygon, brush); } using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -498,6 +474,7 @@ void DrawAction(DrawingCanvas canvas) defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -528,15 +505,14 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi string text = new('A', 200); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => - canvas.DrawText(textOptions, text, drawingOptions, brush, pen: null); + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, null); using Image defaultImage = provider.GetImage(); - RenderWithDefaultBackend(defaultImage, DrawAction); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using Image cpuRegionImage = provider.GetImage(); using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, DrawAction); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -544,6 +520,7 @@ void DrawAction(DrawingCanvas canvas) => defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + drawingOptions, DrawAction, nativeSurfaceInitialImage); @@ -599,15 +576,15 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes Brush drawBrush = Brushes.Solid(Color.HotPink); Brush clearBrush = Brushes.Solid(Color.White); using Image defaultImage = provider.GetImage(); - using (DrawingCanvas defaultClearCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + using (DrawingCanvas defaultClearCanvas = new(Configuration.Default, GetFrameRegion(defaultImage), clearOptions)) { - defaultClearCanvas.Fill(clearBrush, clearOptions); + defaultClearCanvas.Fill(clearBrush); defaultClearCanvas.Flush(); } - using (DrawingCanvas defaultDrawCanvas = new(Configuration.Default, GetFrameRegion(defaultImage))) + using (DrawingCanvas defaultDrawCanvas = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) { - defaultDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + defaultDrawCanvas.DrawText(textOptions, text, drawBrush, null); defaultDrawCanvas.Flush(); } @@ -616,16 +593,16 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes Configuration cpuRegionConfiguration = Configuration.Default.Clone(); cpuRegionConfiguration.SetDrawingBackend(cpuRegionBackend); - using (DrawingCanvas cpuRegionClearCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage))) + using (DrawingCanvas cpuRegionClearCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage), clearOptions)) { - cpuRegionClearCanvas.Fill(clearBrush, clearOptions); + cpuRegionClearCanvas.Fill(clearBrush); cpuRegionClearCanvas.Flush(); } int cpuRegionComputeBatchesBeforeDraw = cpuRegionBackend.TestingComputePathBatchCount; - using (DrawingCanvas cpuRegionDrawCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage))) + using (DrawingCanvas cpuRegionDrawCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage), drawingOptions)) { - cpuRegionDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + cpuRegionDrawCanvas.DrawText(textOptions, text, drawBrush, null); cpuRegionDrawCanvas.Flush(); } @@ -652,17 +629,17 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes Rectangle targetBounds = defaultImage.Bounds; using (DrawingCanvas nativeSurfaceClearCanvas = - new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface))) + new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), clearOptions)) { - nativeSurfaceClearCanvas.Fill(clearBrush, clearOptions); + nativeSurfaceClearCanvas.Fill(clearBrush); nativeSurfaceClearCanvas.Flush(); } int nativeSurfaceComputeBatchesBeforeDraw = nativeSurfaceBackend.TestingComputePathBatchCount; using (DrawingCanvas nativeSurfaceDrawCanvas = - new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface))) + new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) { - nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawingOptions, drawBrush, pen: null); + nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawBrush, null); nativeSurfaceDrawCanvas.Flush(); } @@ -708,10 +685,10 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes } } - private static void RenderWithDefaultBackend(Image image, Action> drawAction) + private static void RenderWithDefaultBackend(Image image, DrawingOptions options, Action> drawAction) where TPixel : unmanaged, IPixel { - using DrawingCanvas canvas = new(Configuration.Default, GetFrameRegion(image)); + using DrawingCanvas canvas = new(Configuration.Default, GetFrameRegion(image), options); drawAction(canvas); canvas.Flush(); } @@ -719,12 +696,13 @@ private static void RenderWithDefaultBackend(Image image, Action private static void RenderWithCpuRegionWebGpuBackend( Image image, WebGPUDrawingBackend backend, + DrawingOptions options, Action> drawAction) where TPixel : unmanaged, IPixel { Configuration configuration = Configuration.Default.Clone(); configuration.SetDrawingBackend(backend); - using DrawingCanvas canvas = new(configuration, GetFrameRegion(image)); + using DrawingCanvas canvas = new(configuration, GetFrameRegion(image), options); drawAction(canvas); canvas.Flush(); } @@ -733,6 +711,7 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( int width, int height, WebGPUDrawingBackend backend, + DrawingOptions options, Action> drawAction, Image? initialImage = null) where TPixel : unmanaged, IPixel @@ -757,7 +736,7 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( Rectangle targetBounds = new(0, 0, width, height); using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface)); + new(configuration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), options); if (initialImage is not null) { Assert.True( diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 51001ed70..f092b14da 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -16,17 +16,18 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() { Configuration configuration = new(); CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); using Image image = new(40, 40); Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); - using DrawingCanvas canvas = new(configuration, backend, new CpuCanvasFrame(region)); IPath path = new RectangularPolygon(4, 6, 18, 12); DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, new CpuCanvasFrame(region), options); Brush brushA = Brushes.Solid(Color.Red); Brush brushB = Brushes.Solid(Color.Blue); - canvas.FillPath(path, brushA, options); - canvas.FillPath(path, brushB, options); + canvas.Fill(path, brushA); + canvas.Fill(path, brushB); canvas.Flush(); Assert.True(backend.HasBatch); @@ -44,17 +45,18 @@ public void Flush_WhenAnyBrushUnsupported_DisablesSharedFlushId() { IsBrushSupported = static brush => brush is SolidBrush }; + configuration.SetDrawingBackend(backend); using Image image = new(40, 40); Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); - using DrawingCanvas canvas = new(configuration, backend, new CpuCanvasFrame(region)); IPath pathA = new RectangularPolygon(2, 2, 12, 12); IPath pathB = new RectangularPolygon(18, 18, 12, 12); DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, new CpuCanvasFrame(region), options); - canvas.FillPath(pathA, Brushes.Solid(Color.Red), options); - canvas.FillPath(pathB, Brushes.Horizontal(Color.Blue), options); + canvas.Fill(pathA, Brushes.Solid(Color.Red)); + canvas.Fill(pathB, Brushes.Horizontal(Color.Blue)); canvas.Flush(); Assert.NotEmpty(backend.Batches); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs index 57150ab8d..117618bbc 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs @@ -20,21 +20,21 @@ public void DrawImage_WithRotationTransform_MatchesReference(TestImagePr using Image foreground = provider.GetImage(); using Image target = new(384, 256); - using DrawingCanvas canvas = new( - provider.Configuration, - new Buffer2DRegion(target.Frames.RootFrame.PixelBuffer, target.Bounds)); - DrawingOptions options = new() { Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) }; - canvas.Clear(Brushes.Solid(Color.White), options); + using DrawingCanvas canvas = new( + provider.Configuration, + new Buffer2DRegion(target.Frames.RootFrame.PixelBuffer, target.Bounds), + options); + + canvas.Clear(Brushes.Solid(Color.White)); canvas.DrawImage( foreground, foreground.Bounds, new RectangleF(72, 48, 240, 160), - options, KnownResamplers.NearestNeighbor); canvas.Flush(); From 15ade35a333641ea5f9a0f29b7c5a481157919bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 00:26:11 +1000 Subject: [PATCH 035/136] SIMD-accelerate coverage application; bump Fonts --- .../ImageSharp.Drawing.csproj | 2 +- .../Backends/DefaultDrawingBackend.cs | 139 +++++++++++++++++- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index e65da8e05..f96862838 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 625e381a6..fb6ccfa68 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -8,13 +9,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// CPU fallback backend that executes path coverage rasterization and brush composition directly against a CPU region. +/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region. /// /// /// -/// This backend is the correctness baseline for all composition behavior. It is also used as the -/// fallback path by GPU backends when the target surface, pixel format, or brush command cannot be -/// executed directly on the GPU. +/// This backend provides the reference CPU implementation for composition behavior. /// /// /// Flush execution is intentionally split: @@ -244,7 +243,137 @@ public void Invoke(int y) Span rowCoverage = this.coverageMap.DangerousGetRowSpan(sourceStartY + y); Span rowSlice = rowCoverage.Slice(sourceStartX, command.DestinationRegion.Width); - this.applicators[i].Apply(rowSlice, destinationX, destinationY + y); + ApplyCoverageSpans(this.applicators[i], rowSlice, destinationX, destinationY + y); + } + } + + /// + /// Applies only contiguous non-zero coverage spans for a scanline. + /// + /// Brush applicator used to composite pixels. + /// Scanline coverage values for the current command row. + /// Destination x coordinate for the start of . + /// Destination y coordinate for the scanline. + private static void ApplyCoverageSpans( + BrushApplicator applicator, + Span coverage, + int destinationX, + int destinationY) + { + // Use SIMD path when available and the span is large enough to amortize setup. + if (Vector.IsHardwareAccelerated && coverage.Length >= (Vector.Count * 2)) + { + ApplyCoverageSpansSimd(applicator, coverage, destinationX, destinationY); + return; + } + + ApplyCoverageSpansScalar(applicator, coverage, destinationX, destinationY); + } + + /// + /// Applies contiguous non-zero coverage spans using SIMD-accelerated zero/non-zero chunk checks. + /// + /// Brush applicator used to composite pixels. + /// Scanline coverage values for the current command row. + /// Destination x coordinate for the start of . + /// Destination y coordinate for the scanline. + private static void ApplyCoverageSpansSimd( + BrushApplicator applicator, + Span coverage, + int destinationX, + int destinationY) + { + int i = 0; + int n = coverage.Length; + int width = Vector.Count; + Vector zero = Vector.Zero; + + while (i < n) + { + // Phase 1: skip fully-zero SIMD blocks. + while (i <= n - width) + { + Vector v = new(coverage.Slice(i, width)); + if (!Vector.EqualsAll(v, zero)) + { + break; + } + + i += width; + } + + while (i < n && coverage[i] == 0F) + { + i++; + } + + if (i >= n) + { + return; + } + + int runStart = i; + + // Phase 2: advance across fully non-zero SIMD blocks. + while (i <= n - width) + { + Vector v = new(coverage.Slice(i, width)); + Vector eqZero = Vector.Equals(v, zero); + if (!Vector.EqualsAll(eqZero, Vector.Zero)) + { + break; + } + + i += width; + } + + while (i < n && coverage[i] != 0F) + { + i++; + } + + // Apply exactly one contiguous non-zero run. + applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY); + } + } + + /// + /// Applies contiguous non-zero coverage spans using a scalar scan. + /// + /// Brush applicator used to composite pixels. + /// Scanline coverage values for the current command row. + /// Destination x coordinate for the start of . + /// Destination y coordinate for the scanline. + private static void ApplyCoverageSpansScalar( + BrushApplicator applicator, + Span coverage, + int destinationX, + int destinationY) + { + // Track the start of a contiguous non-zero coverage run. + int runStart = -1; + for (int i = 0; i < coverage.Length; i++) + { + if (coverage[i] > 0F) + { + // Enter a new run when transitioning from zero to non-zero coverage. + if (runStart < 0) + { + runStart = i; + } + } + else if (runStart >= 0) + { + // Coverage returned to zero: apply the finished run only. + applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY); + runStart = -1; + } + } + + if (runStart >= 0) + { + // Flush trailing run that reaches end-of-scanline. + applicator.Apply(coverage[runStart..], destinationX + runStart, destinationY); } } } From ad121a470926c6e6861e000b19184434ca5a7eba Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 09:35:58 +1000 Subject: [PATCH 036/136] Replace scoped state with save/restore stack --- .../Processing/DrawingCanvasState.cs | 30 ++--- .../Processing/DrawingCanvas{TPixel}.cs | 108 ++++++++++++++---- 2 files changed, 94 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs index c6a4e5f38..1b3e497c2 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -4,48 +4,32 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Immutable drawing state used by . +/// Immutable drawing state snapshot used by . /// -public sealed class DrawingCanvasState : IDisposable +public sealed class DrawingCanvasState { - private readonly Action? releaseScopedState; - private bool disposed; - /// /// Initializes a new instance of the class. /// /// Drawing options for this state. /// Clip paths for this state. - /// Optional callback invoked when a scoped state is disposed. - internal DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths, Action? releaseScopedState = null) + internal DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths) { - Guard.NotNull(options, nameof(options)); - Guard.NotNull(clipPaths, nameof(clipPaths)); - this.Options = options; this.ClipPaths = clipPaths; - this.releaseScopedState = releaseScopedState; } /// /// Gets drawing options associated with this state. /// + /// + /// This is the original reference supplied to the state. + /// It is not deep-cloned. + /// public DrawingOptions Options { get; } /// /// Gets clip paths associated with this state. /// public IReadOnlyList ClipPaths { get; } - - /// - public void Dispose() - { - if (this.disposed) - { - return; - } - - this.releaseScopedState?.Invoke(); - this.disposed = true; - } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index e58f5d662..9d3b75a8d 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -45,14 +45,14 @@ public sealed class DrawingCanvas : IDisposable private readonly List> pendingImageResources = []; /// - /// Represents the default state configuration for the drawing canvas. + /// Tracks whether this instance has already been disposed. /// - private readonly DrawingCanvasState defaultState; + private bool isDisposed; /// - /// Tracks whether this instance has already been disposed. + /// Stack of saved drawing states for Save/Restore operations. /// - private bool isDisposed; + private readonly Stack savedStates = new(); /// /// Initializes a new instance of the class. @@ -141,7 +141,7 @@ private DrawingCanvas( this.targetFrame = targetFrame; this.batcher = batcher; this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); - this.defaultState = defaultState; + this.savedStates.Push(defaultState); } /// @@ -150,10 +150,81 @@ private DrawingCanvas( public Rectangle Bounds { get; } /// - /// Gets or sets the current state of the drawing canvas within the current scope. - /// The value may be if no state is set. + /// Gets the number of saved states currently on the canvas stack. + /// + public int SaveCount => this.savedStates.Count; + + /// + /// Saves the current drawing state on the state stack. + /// + /// + /// This operation stores the current reference. + /// The state is not deep-cloned. If the same instance is + /// mutated after , those mutations are visible when restoring. + /// + /// The save count after the state has been pushed. + public int Save() + { + this.EnsureNotDisposed(); + this.savedStates.Push(this.ResolveState()); + return this.savedStates.Count; + } + + /// + /// Saves the current drawing state and replaces the active state with the provided options and clip paths. + /// + /// + /// The provided instance is stored by reference. + /// Mutating it after this call mutates the active/restored state behavior. + /// + /// Drawing options for the new active state. + /// Clip paths for the new active state. + /// The save count after the previous state has been pushed. + public int Save(DrawingOptions options, params IPath[] clipPaths) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + _ = this.Save(); + DrawingCanvasState state = new(options, clipPaths); + _ = this.savedStates.Pop(); + this.savedStates.Push(state); + return this.savedStates.Count; + } + + /// + /// Restores the most recently saved state. /// - internal DrawingCanvasState? ScopedState { get; set; } + public void Restore() + { + this.EnsureNotDisposed(); + if (this.savedStates.Count <= 1) + { + return; + } + + _ = this.savedStates.Pop(); + } + + /// + /// Restores to a specific save count. + /// + /// + /// State frames above are discarded, + /// and the last discarded frame becomes the current state. + /// + /// The save count to restore to. + public void RestoreTo(int saveCount) + { + this.EnsureNotDisposed(); + Guard.MustBeBetweenOrEqualTo(saveCount, 1, this.savedStates.Count, nameof(saveCount)); + + while (this.savedStates.Count > saveCount) + { + _ = this.savedStates.Pop(); + } + } /// /// Creates a child canvas over a subregion in local coordinates. @@ -166,7 +237,7 @@ public DrawingCanvas CreateRegion(Rectangle region) Rectangle clipped = Rectangle.Intersect(this.Bounds, region); ICanvasFrame childFrame = new CanvasRegionFrame(this.targetFrame, clipped); - return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.defaultState); + return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.ResolveState()); } /// @@ -177,13 +248,8 @@ public DrawingCanvas CreateRegion(Rectangle region) /// The active scoped state that restores to default when disposed. public DrawingCanvasState CreateState(DrawingOptions options, params IPath[] clipPaths) { - this.EnsureNotDisposed(); - Guard.NotNull(options, nameof(options)); - Guard.NotNull(clipPaths, nameof(clipPaths)); - - DrawingCanvasState state = new(options, clipPaths, () => this.ScopedState = null); - this.ScopedState = state; - return state; + _ = this.Save(options, clipPaths); + return this.ResolveState(); } /// @@ -606,8 +672,8 @@ private void DrawTextOperations( /// /// Resolves the currently active drawing state. /// - /// The scoped state when present; otherwise the default state. - private DrawingCanvasState ResolveState() => this.ScopedState ?? this.defaultState; + /// The current state. + private DrawingCanvasState ResolveState() => this.savedStates.Peek(); /// /// Executes an action with a temporary scoped state, restoring the previous scoped state afterwards. @@ -617,15 +683,15 @@ private void DrawTextOperations( /// Action to execute. private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) { - DrawingCanvasState? previous = this.ScopedState; - this.ScopedState = new DrawingCanvasState(options, clipPaths); + int saveCount = this.savedStates.Count; + _ = this.Save(options, [.. clipPaths]); try { action(); } finally { - this.ScopedState = previous; + this.RestoreTo(saveCount); } } From cc0777c712d0f26bee095632c5e981c621565737 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 10:14:30 +1000 Subject: [PATCH 037/136] Add text measurement APIs to DrawingCanvas --- .../Processing/DrawingCanvas{TPixel}.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 9d3b75a8d..a1c583639 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -140,6 +141,8 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; + + // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); this.savedStates.Push(defaultState); } @@ -473,6 +476,132 @@ public void DrawText( this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); } + /// + /// Measures the advance box of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured advance as a rectangle in px units. + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); + return RectangleF.FromLTRB(0, 0, advance.Width, advance.Height); + } + + /// + /// Measures the tight bounds of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured bounds rectangle in px units. + public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + FontRectangle bounds = TextMeasurer.MeasureBounds(text, textOptions); + return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); + } + + /// + /// Measures the size of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured size as a rectangle in px units. + public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + FontRectangle size = TextMeasurer.MeasureSize(text, textOptions); + return RectangleF.FromLTRB(0, 0, size.Width, size.Height); + } + + /// + /// Tries to measure per-character advances for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character advance metrics in px units. + /// if all character advances were measured; otherwise . + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.TryMeasureCharacterAdvances(text, textOptions, out advances); + } + + /// + /// Tries to measure per-character bounds for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character bounds in px units. + /// if all character bounds were measured; otherwise . + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds); + } + + /// + /// Tries to measure per-character sizes for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character sizes in px units. + /// if all character sizes were measured; otherwise . + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.TryMeasureCharacterSizes(text, textOptions, out sizes); + } + + /// + /// Counts the rendered text lines for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The number of rendered lines. + public int CountTextLines(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.CountLines(text, textOptions); + } + + /// + /// Gets line metrics for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// An array of line metrics in px units. + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.GetLineMetrics(text, textOptions); + } + /// /// Draws an image source region into a destination rectangle. /// From b86a39ad5c7ecbfd90fe739246e97df7bea7b5a9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 20:48:08 +1000 Subject: [PATCH 038/136] Use RowStride and DangerousTryGetSingleMemory --- .../WebGPUDrawingBackend.cs | 29 +-------------- .../WebGPUFlushContext.cs | 7 ++-- .../ImageSharp.Drawing.csproj | 8 ++-- .../Processing/DrawingCanvasState.cs | 4 +- .../Processing/DrawingCanvas{TPixel}.cs | 18 ++------- .../PolygonGeometry/ClipperException.cs | 37 ------------------- 6 files changed, 15 insertions(+), 88 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index ece0df386..75f5e96f9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -2018,12 +2018,12 @@ private bool TryReadBackBufferToRegion( try { ReadOnlySpan sourceData = new(mappedData, readbackByteCount); - int destinationStrideBytes = checked(destinationRegion.Buffer.Width * Unsafe.SizeOf()); + int destinationStrideBytes = checked(destinationRegion.Buffer.RowStride * Unsafe.SizeOf()); // Fast path for contiguous full-width rows. if (copyBounds.X == 0 && copyBounds.Width == destinationRegion.Width && - TryGetSingleMemory(destinationRegion.Buffer, out Memory contiguousDestination)) + destinationRegion.Buffer.DangerousTryGetSingleMemory(out Memory contiguousDestination)) { Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); int destinationStart = checked((destinationRegion.Rectangle.Y + copyBounds.Y) * destinationStrideBytes); @@ -2123,31 +2123,6 @@ public void Dispose() private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); - /// - /// Returns whether the 2D buffer is backed by a single contiguous memory segment. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSingleMemory(Buffer2D buffer) - where T : struct - => buffer.MemoryGroup.Count == 1; - - /// - /// Returns the single contiguous memory segment of the provided buffer when available. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetSingleMemory(Buffer2D buffer, out Memory memory) - where T : struct - { - if (!IsSingleMemory(buffer)) - { - memory = default; - return false; - } - - memory = buffer.MemoryGroup[0]; - return true; - } - /// /// Waits for a GPU callback signal, polling the device when the WGPU extension is available. /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 58f8102a0..2b1bc3201 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -930,16 +930,16 @@ internal static void UploadTextureFromRegion( int rowBytes = checked(sourceRegion.Width * pixelSizeInBytes); uint alignedRowBytes = AlignTo256((uint)rowBytes); - if (sourceRegion.Buffer.MemoryGroup.Count == 1) + if (sourceRegion.Buffer.DangerousTryGetSingleMemory(out Memory sourceMemory)) { - int sourceStrideBytes = checked(sourceRegion.Buffer.Width * pixelSizeInBytes); + int sourceStrideBytes = checked(sourceRegion.Buffer.RowStride * pixelSizeInBytes); long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + rowBytes; long packedByteCountEstimate = (long)alignedRowBytes * sourceRegion.Height; // Only use the direct path when the stride satisfies WebGPU's alignment requirement. if ((uint)sourceStrideBytes == alignedRowBytes && directByteCount <= packedByteCountEstimate * 2) { - int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.Width) + sourceRegion.Rectangle.X); + int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.RowStride) + sourceRegion.Rectangle.X); int startByteOffset = checked(startPixelIndex * pixelSizeInBytes); int uploadByteCount = checked((int)directByteCount); nuint uploadByteCountNuint = checked((nuint)uploadByteCount); @@ -951,7 +951,6 @@ internal static void UploadTextureFromRegion( RowsPerImage = (uint)sourceRegion.Height }; - Memory sourceMemory = sourceRegion.Buffer.MemoryGroup[0]; Span sourceBytes = MemoryMarshal.AsBytes(sourceMemory.Span).Slice(startByteOffset, uploadByteCount); fixed (byte* uploadPtr = sourceBytes) { diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index f96862838..89139a468 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -13,6 +13,7 @@ An extension to ImageSharp that allows the drawing of images, paths, and text. Debug;Release true + @@ -25,7 +26,7 @@ enable Nullable - + @@ -43,16 +44,17 @@ - + - + + diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs index 1b3e497c2..24a3821b1 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -6,14 +6,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Immutable drawing state snapshot used by . /// -public sealed class DrawingCanvasState +internal sealed class DrawingCanvasState { /// /// Initializes a new instance of the class. /// /// Drawing options for this state. /// Clip paths for this state. - internal DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths) + public DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths) { this.Options = options; this.ClipPaths = clipPaths; diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index a1c583639..9b9c340e6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -161,9 +161,9 @@ private DrawingCanvas( /// Saves the current drawing state on the state stack. /// /// - /// This operation stores the current reference. - /// The state is not deep-cloned. If the same instance is - /// mutated after , those mutations are visible when restoring. + /// This operation stores the current canvas state by reference. + /// If the same instance is mutated after + /// , those mutations are visible when restoring. /// /// The save count after the state has been pushed. public int Save() @@ -243,18 +243,6 @@ public DrawingCanvas CreateRegion(Rectangle region) return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.ResolveState()); } - /// - /// Creates an immutable scoped drawing state and applies it to this canvas until disposed. - /// - /// Drawing options for the scoped state. - /// Clip paths associated with the scoped state. - /// The active scoped state that restores to default when disposed. - public DrawingCanvasState CreateState(DrawingOptions options, params IPath[] clipPaths) - { - _ = this.Save(options, clipPaths); - return this.ResolveState(); - } - /// /// Clears the whole canvas using the given brush and clear-style composition options. /// diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs deleted file mode 100644 index d22aff793..000000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// The exception that is thrown when an error occurs clipping a polygon. -/// -public class ClipperException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public ClipperException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ClipperException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a - /// reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a - /// reference if no inner exception is specified. - public ClipperException(string message, Exception innerException) - : base(message, innerException) - { - } -} From 4b74a3f41579bd949825a9b29c96b5ed699cd898 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 23:24:21 +1000 Subject: [PATCH 039/136] Add ProcessWithCanvas and DrawingCanvas factories --- .../Processing/DrawingCanvas{TPixel}.cs | 88 ++++++++ .../Extensions/ProcessWithCanvasExtensions.cs | 53 +++++ .../Drawing/ProcessWithCanvasProcessor.cs | 54 +++++ .../ProcessWithCanvasProcessor{TPixel}.cs | 42 ++++ .../Drawing/ProcessWithCanvas.cs | 39 ++++ .../Processing/DrawingCanvasDrawImageTests.cs | 44 ---- .../DrawingCanvasTests.BrushAndPenStyles.cs | 81 +++++++ .../Processing/DrawingCanvasTests.Clear.cs | 64 ++++++ .../DrawingCanvasTests.DrawImage.cs | 106 +++++++++ .../Processing/DrawingCanvasTests.Factory.cs | 63 ++++++ .../Processing/DrawingCanvasTests.Guards.cs | 54 +++++ .../DrawingCanvasTests.PathBuilderDraw.cs | 34 +++ .../DrawingCanvasTests.PathBuilderFill.cs | 34 +++ .../DrawingCanvasTests.PathRules.cs | 76 +++++++ .../DrawingCanvasTests.Primitives.cs | 48 ++++ .../DrawingCanvasTests.RegionAndState.cs | 176 +++++++++++++++ .../DrawingCanvasTests.StrokeOptions.cs | 70 ++++++ .../Processing/DrawingCanvasTests.Text.cs | 212 ++++++++++++++++++ .../Processing/DrawingCanvasTests.cs | 42 ++++ .../ProcessWithCanvasExtensionsTests.cs | 50 +++++ ..._RegionAndPath_MatchesReference_Rgba32.png | 3 + ...r_WithClipPath_MatchesReference_Rgba32.png | 3 + ...calCoordinates_MatchesReference_Rgba32.png | 3 + ...StateIsolation_MatchesReference_Rgba32.png | 3 + ...thAndTransform_MatchesReference_Rgba32.png | 3 + ...ationTransform_MatchesReference_Rgba32.png | 3 + ...pingAndScaling_MatchesReference_Rgba32.png | 3 + ...imitiveHelpers_MatchesReference_Rgba32.png | 3 + ...PathWithOrigin_MatchesReference_Rgba32.png | 3 + ..._FillAndStroke_MatchesReference_Rgba32.png | 3 + ...eMetricsGuides_MatchesReference_Rgba32.png | 3 + ...awText_PenOnly_MatchesReference_Rgba32.png | 3 + ...AndLineSpacing_MatchesReference_Rgba32.png | 3 + ...izeOutputFalse_MatchesReference_Rgba32.png | 3 + ...aw_PathBuilder_MatchesReference_Rgba32.png | 3 + ...ndGradientPens_MatchesReference_Rgba32.png | 3 + ...ll_PathBuilder_MatchesReference_Rgba32.png | 3 + ...enOddVsNonZero_MatchesReference_Rgba32.png | 3 + ...PatternBrushes_MatchesReference_Rgba32.png | 3 + ...MultipleStates_MatchesReference_Rgba32.png | 3 + ...store_ClipPath_MatchesReference_Rgba32.png | 3 + 41 files changed, 1449 insertions(+), 44 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs create mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs create mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 9b9c340e6..704d47a40 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable CA1000 // Do not declare static members on generic types + using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; @@ -157,6 +159,70 @@ private DrawingCanvas( /// public int SaveCount => this.savedStates.Count; + /// + /// Creates a drawing canvas over an existing frame. + /// + /// The frame backing the canvas. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas FromFrame( + ImageFrame frame, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return new DrawingCanvas( + frame.Configuration, + new Buffer2DRegion(frame.PixelBuffer, frame.Bounds), + options, + clipPaths); + } + + /// + /// Creates a drawing canvas over a specific frame of an image. + /// + /// The image containing the frame. + /// The zero-based frame index to target. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting the selected frame. + public static DrawingCanvas FromImage( + Image image, + int frameIndex, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + Guard.MustBeBetweenOrEqualTo(frameIndex, 0, image.Frames.Count - 1, nameof(frameIndex)); + + return FromFrame(image.Frames[frameIndex], options, clipPaths); + } + + /// + /// Creates a drawing canvas over the root frame of an image. + /// + /// The image whose root frame should be targeted. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting the root frame. + public static DrawingCanvas FromRootFrame( + Image image, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return FromFrame(image.Frames.RootFrame, options, clipPaths); + } + /// /// Saves the current drawing state on the state stack. /// @@ -307,6 +373,17 @@ public void Fill(Brush brush, IPathCollection paths) } } + /// + /// Fills a path built by the provided builder using the given brush. + /// + /// The path builder describing the fill region. + /// Brush used to shade covered pixels. + public void Fill(PathBuilder pathBuilder, Brush brush) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Fill(pathBuilder.Build(), brush); + } + /// /// Fills a path in local coordinates using the given brush. /// @@ -397,6 +474,17 @@ public void Draw(Pen pen, IPathCollection paths) } } + /// + /// Draws a path outline built by the provided builder using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path builder describing the path to stroke. + public void Draw(Pen pen, PathBuilder pathBuilder) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Draw(pen, pathBuilder.Build()); + } + /// /// Draws a path outline in local coordinates using the given pen. /// diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs new file mode 100644 index 000000000..dab8a1f44 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents a drawing callback executed against a . +/// +/// The pixel format. +/// The drawing canvas for the current frame. +public delegate void CanvasAction(DrawingCanvas canvas) + where TPixel : unmanaged, IPixel; + +/// +/// Adds extensions that execute drawing callbacks against all frames through . +/// +public static class ProcessWithCanvasExtensions +{ + /// + /// Executes for each image frame using drawing options from the current context. + /// + /// The pixel format expected by the callback. + /// The source image processing context. + /// The drawing callback to execute for each frame. + /// The to allow chaining of operations. + public static IImageProcessingContext ProcessWithCanvas( + this IImageProcessingContext source, + CanvasAction action) + where TPixel : unmanaged, IPixel + => source.ProcessWithCanvas(source.GetDrawingOptions(), action); + + /// + /// Executes for each image frame using the supplied drawing options. + /// + /// The pixel format expected by the callback. + /// The source image processing context. + /// The drawing options. + /// The drawing callback to execute for each frame. + /// The to allow chaining of operations. + public static IImageProcessingContext ProcessWithCanvas( + this IImageProcessingContext source, + DrawingOptions options, + CanvasAction action) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(action, nameof(action)); + + return source.ApplyProcessor(new ProcessWithCanvasProcessor(options, typeof(TPixel), action)); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs new file mode 100644 index 000000000..a86ade0df --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +/// +/// Defines a processor that executes a canvas callback for each image frame. +/// +public sealed class ProcessWithCanvasProcessor : IImageProcessor +{ + /// + /// Initializes a new instance of the class. + /// + /// The drawing options. + /// The pixel type expected by . + /// The per-frame canvas callback. + public ProcessWithCanvasProcessor(DrawingOptions options, Type pixelType, Delegate action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(pixelType, nameof(pixelType)); + Guard.NotNull(action, nameof(action)); + + this.Options = options; + this.PixelType = pixelType; + this.Action = action; + } + + /// + /// Gets the drawing options. + /// + public DrawingOptions Options { get; } + + internal Type PixelType { get; } + + internal Delegate Action { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor( + Configuration configuration, + Image source, + Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + { + if (typeof(TPixel) != this.PixelType) + { + throw new InvalidOperationException( + $"ProcessWithCanvas expects pixel type '{this.PixelType.Name}' but the image uses '{typeof(TPixel).Name}'."); + } + + return new ProcessWithCanvasProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs new file mode 100644 index 000000000..cb0d76dc6 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; + +/// +/// Executes a per-frame canvas callback for a specific pixel type. +/// +/// The pixel format. +internal sealed class ProcessWithCanvasProcessor : ImageProcessor + where TPixel : unmanaged, IPixel +{ + private readonly ProcessWithCanvasProcessor definition; + private readonly CanvasAction action; + + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The processor definition. + /// The source image. + /// The source bounds. + public ProcessWithCanvasProcessor( + Configuration configuration, + ProcessWithCanvasProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + this.action = (CanvasAction)definition.Action; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using DrawingCanvas canvas = DrawingCanvas.FromFrame(source, this.definition.Options); + this.action(canvas); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs new file mode 100644 index 000000000..689806c87 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; +using SixLabors.ImageSharp.Drawing.Tests.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; + +public class ProcessWithCanvas : BaseImageOperationsExtensionTest +{ + private readonly DrawingOptions nonDefaultOptions = new(); + + [Fact] + public void CanvasActionDefaultOptions() + { + this.operations.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Red))); + + ProcessWithCanvasProcessor processor = this.Verify(); + + GraphicsOptions expectedOptions = this.graphicsOptions; + Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); + Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); + Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); + Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); + } + + [Fact] + public void CanvasActionWithOptions() + { + this.operations.ProcessWithCanvas( + this.nonDefaultOptions, + canvas => canvas.Clear(Brushes.Solid(Color.Red))); + + ProcessWithCanvasProcessor processor = this.Verify(); + Assert.Equal(this.nonDefaultOptions, processor.Options); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs deleted file mode 100644 index 117618bbc..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasDrawImageTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Processing; - -[GroupOutput("Drawing")] -public class DrawingCanvasDrawImageTests -{ - [Theory] - [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] - public void DrawImage_WithRotationTransform_MatchesReference(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image foreground = provider.GetImage(); - using Image target = new(384, 256); - - DrawingOptions options = new() - { - Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) - }; - - using DrawingCanvas canvas = new( - provider.Configuration, - new Buffer2DRegion(target.Frames.RootFrame.PixelBuffer, target.Bounds), - options); - - canvas.Clear(Brushes.Solid(Color.White)); - canvas.DrawImage( - foreground, - foreground.Bounds, - new RectangleF(72, 48, 240, 160), - KnownResamplers.NearestNeighbor); - canvas.Flush(); - - target.DebugSave(provider, appendSourceFileOrDescription: false); - target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs new file mode 100644 index 000000000..59bf3d9e0 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Fill_WithGradientAndPatternBrushes_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Brush linearBrush = new LinearGradientBrush( + new PointF(18, 22), + new PointF(192, 140), + GradientRepetitionMode.None, + new ColorStop(0F, Color.LightYellow), + new ColorStop(0.5F, Color.DeepSkyBlue.WithAlpha(0.85F)), + new ColorStop(1F, Color.MediumBlue.WithAlpha(0.9F))); + + Brush radialBrush = new RadialGradientBrush( + new PointF(238, 88), + 66F, + GradientRepetitionMode.Reflect, + new ColorStop(0F, Color.Orange.WithAlpha(0.95F)), + new ColorStop(1F, Color.MediumVioletRed.WithAlpha(0.25F))); + + Brush hatchBrush = Brushes.ForwardDiagonal(Color.DarkSlateGray.WithAlpha(0.7F), Color.Transparent); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(14, 14, 176, 126), linearBrush); + canvas.Fill(new EllipsePolygon(new PointF(236, 90), new SizeF(132, 98)), radialBrush); + canvas.Fill(CreateClosedPathBuilder(), hatchBrush); + canvas.Draw(Pens.DashDot(Color.Black, 3), new Rectangle(10, 10, 300, 180)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Draw_WithPatternAndGradientPens_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Brush gradientBrush = new LinearGradientBrush( + new PointF(0, 0), + new PointF(320, 0), + GradientRepetitionMode.Repeat, + new ColorStop(0F, Color.CornflowerBlue), + new ColorStop(0.5F, Color.Gold), + new ColorStop(1F, Color.MediumSeaGreen)); + + Brush patternBrush = Brushes.Vertical(Color.DarkRed.WithAlpha(0.75F), Color.Transparent); + Brush percentBrush = Brushes.Percent20(Color.DarkOrange.WithAlpha(0.85F), Color.Transparent); + + Pen dashPen = Pens.Dash(gradientBrush, 6F); + Pen dotPen = Pens.Dot(patternBrush, 5F); + Pen dashDotPen = Pens.DashDot(percentBrush, 4F); + Pen dashDotDotPen = Pens.DashDotDot(Color.Black.WithAlpha(0.75F), 3F); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(dashPen, new Rectangle(16, 14, 288, 170)); + canvas.DrawEllipse(dotPen, new PointF(162, 100), new SizeF(206, 116)); + canvas.DrawArc(dashDotPen, new PointF(160, 100), new SizeF(148, 84), rotation: 0, startAngle: 20, sweepAngle: 300); + canvas.DrawLine(dashDotDotPen, new PointF(26, 174), new PointF(108, 22), new PointF(212, 164), new PointF(292, 26)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs new file mode 100644 index 000000000..7cdab4b14 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(256, 160, PixelTypes.Rgba32)] + public void Clear_RegionAndPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); + canvas.Fill(new Rectangle(22, 16, 188, 118), Brushes.Solid(Color.Crimson.WithAlpha(0.8F))); + canvas.DrawEllipse(Pens.Solid(Color.Gold, 5), new PointF(128, 80), new SizeF(140, 90)); + + canvas.Clear(new Rectangle(56, 36, 108, 64), Brushes.Solid(Color.LightYellow.WithAlpha(0.45F))); + IPath clearPath = new EllipsePolygon(new PointF(178, 80), new SizeF(74, 56)); + canvas.Clear(clearPath, Brushes.Solid(Color.Transparent)); + + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(10, 10, 236, 140)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Clear_WithClipPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(0, 0, 320, 200), Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); + canvas.Fill(new Rectangle(26, 18, 268, 164), Brushes.Solid(Color.Crimson.WithAlpha(0.78F))); + canvas.DrawEllipse(Pens.Solid(Color.Gold, 5F), new PointF(160, 100), new SizeF(196, 116)); + + IPath clipPath = new EllipsePolygon(new PointF(160, 100), new SizeF(214, 126)); + _ = canvas.Save(new DrawingOptions(), clipPath); + + canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.85F))); + canvas.Clear(new Rectangle(40, 24, 108, 72), Brushes.Solid(Color.MediumPurple.WithAlpha(0.72F))); + canvas.Clear(new Rectangle(172, 96, 110, 70), Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F))); + canvas.Clear(new EllipsePolygon(new PointF(164, 98), new SizeF(74, 48)), Brushes.Solid(Color.Transparent)); + + canvas.Restore(); + + canvas.Draw(Pens.DashDot(Color.Black, 3F), clipPath); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 304, 184)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs new file mode 100644 index 000000000..07f4e00ed --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void DrawImage_WithRotationTransform_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(384, 256); + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage( + foreground, + foreground.Bounds, + new RectangleF(72, 48, 240, 160), + KnownResamplers.NearestNeighbor); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(320, 220, PixelTypes.Rgba32)] + public void DrawImage_WithSourceClippingAndScaling_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(320, 220); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage( + foreground, + new Rectangle(-48, 18, 196, 148), + new RectangleF(18, 20, 170, 120), + KnownResamplers.Bicubic); + canvas.DrawImage( + foreground, + new Rectangle(220, 100, 160, 140), + new RectangleF(170, 72, 130, 110), + KnownResamplers.NearestNeighbor); + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(8, 8, 304, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(360, 240, PixelTypes.Rgba32)] + public void DrawImage_WithClipPathAndTransform_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(360, 240); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + DrawingOptions transformedOptions = new() + { + Transform = Matrix3x2.CreateRotation(0.32F, new Vector2(180, 120)) + }; + + IPath clipPath = new EllipsePolygon(new PointF(180, 120), new SizeF(208, 126)); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(18, 16, 324, 208), Brushes.Solid(Color.LightGray.WithAlpha(0.45F))); + + _ = canvas.Save(transformedOptions, clipPath); + canvas.DrawImage( + foreground, + new Rectangle(10, 8, 234, 180), + new RectangleF(64, 36, 232, 164), + KnownResamplers.Bicubic); + canvas.DrawImage( + foreground, + new Rectangle(102, 32, 196, 166), + new RectangleF(92, 58, 210, 148), + KnownResamplers.NearestNeighbor); + canvas.Restore(); + + canvas.Draw(Pens.DashDot(Color.DarkSlateGray, 3F), clipPath); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 224)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs new file mode 100644 index 000000000..bf557661f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void FromFrame_TargetsProvidedFrame() + { + using Image image = new(48, 36); + + using (DrawingCanvas canvas = DrawingCanvas.FromFrame( + image.Frames.RootFrame, + new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.SeaGreen)); + canvas.Flush(); + } + + Assert.Equal(Color.SeaGreen.ToPixel(), image[12, 10]); + } + + [Fact] + public void FromImage_TargetsRequestedFrame() + { + using Image image = new(40, 30); + image.Frames.AddFrame(image.Frames.RootFrame); + + using (DrawingCanvas rootCanvas = DrawingCanvas.FromRootFrame(image, new DrawingOptions())) + { + rootCanvas.Clear(Brushes.Solid(Color.White)); + rootCanvas.Flush(); + } + + using (DrawingCanvas secondCanvas = DrawingCanvas.FromImage(image, 1, new DrawingOptions())) + { + secondCanvas.Clear(Brushes.Solid(Color.MediumPurple)); + secondCanvas.Flush(); + } + + Assert.Equal(Color.White.ToPixel(), image.Frames.RootFrame[8, 8]); + Assert.Equal(Color.MediumPurple.ToPixel(), image.Frames[1][8, 8]); + } + + [Fact] + public void FromImage_InvalidFrameIndex_Throws() + { + using Image image = new(20, 20); + image.Frames.AddFrame(image.Frames.RootFrame); + + ArgumentOutOfRangeException low = Assert.Throws( + () => DrawingCanvas.FromImage(image, -1, new DrawingOptions())); + ArgumentOutOfRangeException high = Assert.Throws( + () => DrawingCanvas.FromImage(image, image.Frames.Count, new DrawingOptions())); + + Assert.Equal("frameIndex", low.ParamName); + Assert.Equal("frameIndex", high.ParamName); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs new file mode 100644 index 000000000..2fbbdb861 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void RestoreTo_InvalidCount_Throws() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas( + provider, + target, + new DrawingOptions()); + + ArgumentOutOfRangeException low = Assert.Throws(() => canvas.RestoreTo(0)); + ArgumentOutOfRangeException high = Assert.Throws(() => canvas.RestoreTo(2)); + + Assert.Equal("saveCount", low.ParamName); + Assert.Equal("saveCount", high.ParamName); + } + + [Fact] + public void Dispose_ThenOperations_ThrowObjectDisposedException() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(96, 96); + using Image source = new(24, 24); + using DrawingCanvas canvas = CreateCanvas( + provider, + target, + new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 16); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(10, 28) + }; + + canvas.Dispose(); + + Assert.Throws(() => canvas.Fill(Brushes.Solid(Color.Black))); + Assert.Throws(() => canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 60, 60))); + Assert.Throws(() => canvas.DrawText(textOptions, "Disposed", Brushes.Solid(Color.DarkBlue), pen: null)); + Assert.Throws(() => canvas.DrawImage(source, source.Bounds, new RectangleF(12, 12, 48, 48))); + Assert.Throws(canvas.Flush); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs new file mode 100644 index 000000000..84eb2fd0a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void Draw_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation(-0.15F, new Vector2(96F, 64F)) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + PathBuilder pathBuilder = CreateOpenPathBuilder(); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.CornflowerBlue, 6F), pathBuilder); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs new file mode 100644 index 000000000..5d684ce85 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void Fill_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation(0.2F, new Vector2(96F, 64F)) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + PathBuilder pathBuilder = CreateClosedPathBuilder(); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(pathBuilder, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs new file mode 100644 index 000000000..e454e4a78 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath leftPath = CreatePentagramPath(new PointF(96, 110), 78F); + IPath rightPath = CreatePentagramPath(new PointF(264, 110), 78F); + + DrawingOptions evenOddOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + DrawingOptions nonZeroOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(12, 12, 336, 196), Brushes.Solid(Color.AliceBlue.WithAlpha(0.7F))); + + _ = canvas.Save(evenOddOptions); + canvas.Fill(leftPath, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Restore(); + + _ = canvas.Save(nonZeroOptions); + canvas.Fill(rightPath, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 3F), leftPath); + canvas.Draw(Pens.Solid(Color.Black, 3F), rightPath); + canvas.DrawLine(Pens.Dash(Color.Gray, 2F), new PointF(180, 20), new PointF(180, 200)); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + private static IPath CreatePentagramPath(PointF center, float radius) + { + PointF[] points = new PointF[5]; + for (int i = 0; i < points.Length; i++) + { + float angle = (-MathF.PI / 2F) + (i * (MathF.PI * 2F / points.Length)); + points[i] = new PointF( + center.X + (radius * MathF.Cos(angle)), + center.Y + (radius * MathF.Sin(angle))); + } + + int[] order = [0, 2, 4, 1, 3, 0]; + PathBuilder builder = new(); + for (int i = 0; i < order.Length - 1; i++) + { + PointF a = points[order[i]]; + PointF b = points[order[i + 1]]; + builder.AddLine(a.X, a.Y, b.X, b.Y); + } + + builder.CloseAllFigures(); + return builder.Build(); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs new file mode 100644 index 000000000..d4dde3da3 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(240, 160, PixelTypes.Rgba32)] + public void DrawPrimitiveHelpers_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs new file mode 100644 index 000000000..c79ff558c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(256, 160, PixelTypes.Rgba32)] + public void CreateRegion_LocalCoordinates_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + using (DrawingCanvas regionCanvas = canvas.CreateRegion(new Rectangle(40, 24, 140, 96))) + { + regionCanvas.Fill(new Rectangle(10, 8, 80, 46), Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F))); + regionCanvas.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 140, 96)); + regionCanvas.DrawLine( + Pens.Solid(Color.OrangeRed, 4), + new PointF(0, 95), + new PointF(139, 0)); + } + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void SaveRestore_ClipPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + IPath clipPath = new EllipsePolygon(new PointF(96, 64), new SizeF(120, 76)); + _ = canvas.Save(new DrawingOptions(), clipPath); + + canvas.Fill(new Rectangle(0, 0, 192, 128), Brushes.Solid(Color.MediumVioletRed.WithAlpha(0.85F))); + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(24, 16, 144, 96)); + + canvas.Restore(); + + canvas.Fill(new Rectangle(0, 96, 192, 32), Brushes.Solid(Color.SteelBlue.WithAlpha(0.75F))); + canvas.Draw(Pens.Solid(Color.DarkGreen, 4), new Rectangle(8, 8, 176, 112)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(224, 160, PixelTypes.Rgba32)] + public void RestoreTo_MultipleStates_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + DrawingOptions firstOptions = new() + { + Transform = Matrix3x2.CreateTranslation(20F, 12F) + }; + + int firstSaveCount = canvas.Save(firstOptions, new RectangularPolygon(20, 20, 144, 104)); + canvas.Fill(new Rectangle(0, 0, 120, 84), Brushes.Solid(Color.SkyBlue.WithAlpha(0.8F))); + + DrawingOptions secondOptions = new() + { + Transform = Matrix3x2.CreateRotation(0.24F, new Vector2(112, 80)) + }; + + _ = canvas.Save(secondOptions, new EllipsePolygon(new PointF(112, 80), new SizeF(130, 90))); + canvas.Draw(Pens.Solid(Color.MediumPurple, 6), new Rectangle(34, 26, 152, 108)); + + canvas.RestoreTo(firstSaveCount); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(0, 100), + new PointF(76, 18), + new PointF(168, 92)); + + canvas.RestoreTo(1); + canvas.Fill(new Rectangle(156, 106, 48, 34), Brushes.Solid(Color.Gold.WithAlpha(0.7F))); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 4), new Rectangle(8, 8, 208, 144)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 220, PixelTypes.Rgba32)] + public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(12, 12, 296, 196), Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F))); + + DrawingOptions rootOptions = new() + { + Transform = Matrix3x2.CreateTranslation(6F, 4F) + }; + + IPath rootClip = new EllipsePolygon(new PointF(160, 110), new SizeF(252, 164)); + _ = canvas.Save(rootOptions, rootClip); + + using (DrawingCanvas outerRegion = canvas.CreateRegion(new Rectangle(30, 24, 240, 156))) + { + outerRegion.Fill(new Rectangle(0, 0, 240, 156), Brushes.Solid(Color.LightBlue.WithAlpha(0.35F))); + outerRegion.Draw(Pens.Solid(Color.DarkBlue, 3F), new Rectangle(0, 0, 240, 156)); + + DrawingOptions outerOptions = new() + { + Transform = Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78)) + }; + + _ = outerRegion.Save(outerOptions, new RectangularPolygon(18, 14, 204, 128)); + outerRegion.Fill(new Rectangle(16, 16, 208, 124), Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F))); + + using (DrawingCanvas innerRegion = outerRegion.CreateRegion(new Rectangle(52, 34, 132, 82))) + { + innerRegion.Clear(Brushes.Solid(Color.LightGoldenrodYellow.WithAlpha(0.8F))); + + DrawingOptions innerOptions = new() + { + Transform = Matrix3x2.CreateSkew(0.18F, 0F) + }; + + _ = innerRegion.Save(innerOptions, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); + innerRegion.Fill(new Rectangle(0, 0, 132, 82), Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F))); + innerRegion.DrawLine( + Pens.Solid(Color.DarkRed, 4F), + new PointF(0, 80), + new PointF(66, 0), + new PointF(132, 74)); + innerRegion.Restore(); + + innerRegion.Draw(Pens.DashDot(Color.Black.WithAlpha(0.75F), 2F), new Rectangle(4, 4, 124, 74)); + } + + outerRegion.Restore(); + + outerRegion.Fill(new Rectangle(8, 112, 90, 30), Brushes.Solid(Color.OrangeRed.WithAlpha(0.6F))); + outerRegion.DrawLine(Pens.Solid(Color.Black, 3F), new PointF(8, 8), new PointF(232, 148)); + } + + canvas.RestoreTo(1); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 3F), new Rectangle(8, 8, 304, 204)); + canvas.DrawLine(Pens.Dash(Color.Gray, 2F), new PointF(20, 200), new PointF(300, 20)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs new file mode 100644 index 000000000..20072c184 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath leftPath = CreateBowTiePath(new RectangleF(28, 34, 128, 152)); + IPath rightPath = CreateBowTiePath(new RectangleF(204, 34, 128, 152)); + + SolidPen nonNormalizedPen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); + nonNormalizedPen.StrokeOptions.NormalizeOutput = false; + nonNormalizedPen.StrokeOptions.LineJoin = LineJoin.Round; + nonNormalizedPen.StrokeOptions.LineCap = LineCap.Round; + + SolidPen normalizedPen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); + normalizedPen.StrokeOptions.NormalizeOutput = true; + normalizedPen.StrokeOptions.LineJoin = LineJoin.Round; + normalizedPen.StrokeOptions.LineCap = LineCap.Round; + + DrawingOptions evenOddOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(12, 12, 336, 196), Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F))); + + _ = canvas.Save(evenOddOptions); + canvas.Draw(nonNormalizedPen, leftPath); + canvas.Draw(normalizedPen, rightPath); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2F), leftPath); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2F), rightPath); + canvas.DrawLine(Pens.DashDot(Color.Gray, 2F), new PointF(180, 20), new PointF(180, 200)); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + private static IPath CreateBowTiePath(RectangleF bounds) + { + float left = bounds.Left; + float right = bounds.Right; + float top = bounds.Top; + float bottom = bounds.Bottom; + + PathBuilder builder = new(); + builder.AddLine(left, top, right, bottom); + builder.AddLine(right, bottom, left, bottom); + builder.AddLine(left, bottom, right, top); + builder.AddLine(right, top, left, top); + builder.CloseAllFigures(); + return builder.Build(); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs new file mode 100644 index 000000000..95a153851 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(760, 320, PixelTypes.Rgba32)] + public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateTranslation(24F, 22F) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 32); + + string text = "Quick wafting zephyrs vex bold Jim.\n" + + "How quickly daft jumping zebras vex.\n" + + "Sphinx of black quartz, judge my vow."; + + RichTextOptions textOptions = new(font) + { + Origin = PointF.Empty, + LineSpacing = 1.45F + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(0, 0, 712, 276), Brushes.Solid(Color.LightSteelBlue.WithAlpha(0.25F))); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + + LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text); + float lineOriginY = textOptions.Origin.Y; + for (int i = 0; i < lineMetrics.Length; i++) + { + LineMetrics metrics = lineMetrics[i]; + float startX = metrics.Start; + float endX = metrics.Start + metrics.Extent; + float topY = lineOriginY; + float ascenderY = lineOriginY + metrics.Ascender; + float baselineY = lineOriginY + metrics.Baseline; + float descenderY = lineOriginY + metrics.Descender; + float lineHeightY = lineOriginY + metrics.LineHeight; + + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(startX, topY), new PointF(endX, topY)); + canvas.DrawLine(Pens.Solid(Color.RoyalBlue.WithAlpha(0.9F), 1), new PointF(startX, ascenderY), new PointF(endX, ascenderY)); + canvas.DrawLine(Pens.Solid(Color.Crimson.WithAlpha(0.9F), 1), new PointF(startX, baselineY), new PointF(endX, baselineY)); + canvas.DrawLine(Pens.Solid(Color.DarkOrange.WithAlpha(0.9F), 1), new PointF(startX, descenderY), new PointF(endX, descenderY)); + canvas.DrawLine(Pens.Solid(Color.SeaGreen.WithAlpha(0.9F), 1), new PointF(startX, lineHeightY), new PointF(endX, lineHeightY)); + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(startX, topY), new PointF(startX, lineHeightY)); + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(endX, topY), new PointF(endX, lineHeightY)); + + lineOriginY += metrics.LineHeight; + } + + canvas.Draw(Pens.Solid(Color.Black, 2), new Rectangle(0, 0, 712, 276)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(420, 220, PixelTypes.Rgba32)] + public void DrawText_FillAndStroke_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation(-0.08F, new Vector2(210, 110)) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 36); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(24, 36), + WrappingLength = 372 + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText( + textOptions, + "Canvas text\nwith fill + stroke", + Brushes.Solid(Color.MidnightBlue.WithAlpha(0.82F)), + Pens.Solid(Color.Gold, 2F)); + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 400, 200)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 180, PixelTypes.Rgba32)] + public void DrawText_PenOnly_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 52); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(18, 42) + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(new Rectangle(12, 14, 296, 152), Brushes.Solid(Color.LightSkyBlue.WithAlpha(0.45F))); + canvas.DrawText(textOptions, "OUTLINE", brush: null, pen: Pens.Solid(Color.SeaGreen, 3.5F)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void DrawText_AlongPathWithOrigin_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath textPath = new EllipsePolygon(new PointF(172, 112), new SizeF(246, 112)); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 21); + RichTextOptions textOptions = new(font) + { + Path = textPath, + Origin = new PointF(16, -10), + WrappingLength = textPath.ComputeLength(), + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Bottom + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.SlateGray, 2), textPath); + canvas.DrawText( + textOptions, + "Sphinx of black quartz, judge my vow.", + Brushes.Solid(Color.DarkRed.WithAlpha(0.9F)), + pen: null); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(840, 420, PixelTypes.Rgba32)] + public void DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 28); + Rectangle layoutBounds = new(120, 50, 600, 320); + + RichTextOptions textOptions = new(font) + { + Origin = new PointF( + layoutBounds.Left + (layoutBounds.Width / 2F), + layoutBounds.Top + (layoutBounds.Height / 2F)), + WrappingLength = layoutBounds.Width - 64F, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + LineSpacing = 2.1F + }; + + string text = + "Pack my box with five dozen liquor jugs while zephyrs drift across the bay.\n" + + "Sphinx of black quartz, judge my vow."; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(layoutBounds, Brushes.Solid(Color.LightGoldenrodYellow.WithAlpha(0.45F))); + canvas.Draw(Pens.Solid(Color.SlateGray, 2F), layoutBounds); + canvas.DrawLine( + Pens.Dash(Color.Gray.WithAlpha(0.8F), 1.5F), + new PointF(textOptions.Origin.X, layoutBounds.Top), + new PointF(textOptions.Origin.X, layoutBounds.Bottom)); + canvas.DrawLine( + Pens.Dash(Color.Gray.WithAlpha(0.8F), 1.5F), + new PointF(layoutBounds.Left, textOptions.Origin.Y), + new PointF(layoutBounds.Right, textOptions.Origin.Y)); + + canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.DarkBlue.WithAlpha(0.86F)), + Pens.Solid(Color.DarkRed.WithAlpha(0.55F), 1.1F)); + + canvas.Draw(Pens.Solid(Color.Black, 3F), new Rectangle(10, 10, 820, 400)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs new file mode 100644 index 000000000..d134819a2 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +[GroupOutput("Drawing")] +public partial class DrawingCanvasTests +{ + private static DrawingCanvas CreateCanvas( + TestImageProvider provider, + Image image, + DrawingOptions options) + where TPixel : unmanaged, IPixel + => new( + provider.Configuration, + image.Frames.RootFrame.PixelBuffer.GetRegion(), + options); + + private static PathBuilder CreateClosedPathBuilder() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(22, 24, 124, 30); + pathBuilder.AddLine(124, 30, 168, 98); + pathBuilder.AddLine(168, 98, 40, 108); + pathBuilder.AddLine(40, 108, 22, 24); + pathBuilder.CloseAllFigures(); + return pathBuilder; + } + + private static PathBuilder CreateOpenPathBuilder() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(20, 98, 54, 22); + pathBuilder.AddLine(54, 22, 114, 76); + pathBuilder.AddLine(114, 76, 170, 26); + return pathBuilder; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs new file mode 100644 index 000000000..1f838f135 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class ProcessWithCanvasExtensionsTests +{ + [Fact] + public void ProcessWithCanvas_Mutate_AppliesToAllFrames() + { + using Image image = new(24, 16); + image.Frames.AddFrame(image.Frames.RootFrame); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.OrangeRed)))); + + Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames.RootFrame[8, 6]); + Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames[1][8, 6]); + } + + [Fact] + public void ProcessWithCanvas_Clone_AppliesToAllFrames_WithoutMutatingSource() + { + using Image source = new(24, 16); + source.Frames.AddFrame(source.Frames.RootFrame); + source.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.White)))); + + using Image clone = source.Clone( + ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.MediumPurple)))); + + Assert.Equal(Color.White.ToPixel(), source.Frames.RootFrame[8, 6]); + Assert.Equal(Color.White.ToPixel(), source.Frames[1][8, 6]); + Assert.Equal(Color.MediumPurple.ToPixel(), clone.Frames.RootFrame[8, 6]); + Assert.Equal(Color.MediumPurple.ToPixel(), clone.Frames[1][8, 6]); + } + + [Fact] + public void ProcessWithCanvas_WhenPixelTypeMismatch_Throws() + { + using Image image = new(12, 12); + + InvalidOperationException ex = Assert.Throws( + () => image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Black))))); + + Assert.Contains("expects pixel type", ex.Message); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..7324dfbe0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63523bfb277d9f0db0c5a58fdd8d8a0a14d26537abece16322325bd9faac1e9a +size 3910 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..46017f65f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:701d483e03920b9a1b9b3b5ddea095118574b7c0716a1d89a6cf5afd86dc6d04 +size 12048 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png new file mode 100644 index 000000000..d68fe1bdc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40b3fbb49e7db2057ddb8c47b4ae5714b7d91314572c8e9406f7c39b4ff13146 +size 3402 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png new file mode 100644 index 000000000..24acb55da --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2350af6f9f632cad14619e397fea7b8ae14cb6f2f6a03d430e096e322a69d231 +size 13870 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png new file mode 100644 index 000000000..874e0d50e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a96553abab4cf7ad4d5c533472f52d0357f961c9eba56cdd182d6664f46abd2 +size 13645 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png new file mode 100644 index 000000000..41e7ad63f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4a94515495d39d337393f563a87eefdf409ab13ac67fc217475b346bf80fb67 +size 4303 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png new file mode 100644 index 000000000..84033c93b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7267c13d86d482f4dfe9def6f884a11ea2cc7878d0cb8dd7774a2cae6191e83f +size 2805 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png new file mode 100644 index 000000000..d7764d164 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb35fc9756721ea6cd3c997df0b890fb728c72b5e44595481b02b769ecabfdbe +size 10869 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png new file mode 100644 index 000000000..a9af24829 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a57a23b6b4de5739698a9af36d65431222452a0e9e6c404916863e69c01bf0 +size 12411 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png new file mode 100644 index 000000000..cf690043b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d9673fef71cbcb6a6eda3e6d89e1716e302efde9bbc1fe9ecb1dd6f30e7eb03 +size 24071 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png new file mode 100644 index 000000000..f747c34a2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:796675139385d990ee507b85fea4326fa0d9338a1f56846cb0d43c52e0733b72 +size 28833 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png new file mode 100644 index 000000000..1af1d92d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ece6df5e1054b3cffd18b4efa34e98cabcae94fd50327091cd570f671c378b9d +size 5352 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png new file mode 100644 index 000000000..47e1947bf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b307e9da1ba763506f474fbc94c5f2bc91a7363c8d5f91bc21d6c8d1518e5a92 +size 50388 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png new file mode 100644 index 000000000..353dd3271 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fcd3c21bf085435a8deedbdc6ebc1a53ee0d8772e7f338fe62b3aeb025324f7 +size 7571 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..7c504510b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff8de261ad72b6d70edb7c9a0e44eff7ed5108d32bbc162aa86a48b4f83851a2 +size 3831 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png new file mode 100644 index 000000000..0f94e9962 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c01d2904133f8a24f6b57517ed9f1df6a9bf9f21d39c52502cb2817f5f79ec15 +size 13259 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..9c1c6a7a5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5639df5a84e3a9731982af35325391bbb7ab24b5add9e45e29a6fad055bf8315 +size 2991 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png new file mode 100644 index 000000000..312bf9c42 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d23d9f940f0a91b16cc77b6a728c13240d18b654818e471ae65df0ba3666e83 +size 10415 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png new file mode 100644 index 000000000..ec1d63e71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a376eee88cf42ca5c76ac36fac5f123602113bb9c3c7cc565b9e16112727a2a7 +size 23632 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png new file mode 100644 index 000000000..b46e34bc6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac5d46871737d28c60a7d9d71fd074fa2e548b3d3223990b31c2c3f21555d6f +size 6138 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..189e5a0e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e81ca1ff7c5f39a6fa517f0a46c1cf986f9569d6f361668c177d56765c61f4ca +size 2650 From 1425632346a3e2a86e8b42ee931bb2748f1242a7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Mar 2026 23:51:47 +1000 Subject: [PATCH 040/136] Add IDrawingCanvas and non-generic ProcessWithCanvas --- .../Processing/DrawingCanvas{TPixel}.cs | 278 +++++------------ .../Extensions/ProcessWithCanvasExtensions.cs | 22 +- .../Processing/IDrawingCanvas.cs | 283 ++++++++++++++++++ .../Drawing/ProcessWithCanvasProcessor.cs | 19 +- .../ProcessWithCanvasProcessor{TPixel}.cs | 4 +- .../Drawing/ProcessWithCanvas.cs | 5 +- .../ProcessWithCanvasExtensionsTests.cs | 31 +- 7 files changed, 404 insertions(+), 238 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 704d47a40..8963050cc 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// A drawing canvas over a frame target. /// /// The pixel format. -public sealed class DrawingCanvas : IDisposable +public sealed class DrawingCanvas : IDrawingCanvas where TPixel : unmanaged, IPixel { /// @@ -149,14 +149,10 @@ private DrawingCanvas( this.savedStates.Push(defaultState); } - /// - /// Gets the local bounds of this canvas. - /// + /// public Rectangle Bounds { get; } - /// - /// Gets the number of saved states currently on the canvas stack. - /// + /// public int SaveCount => this.savedStates.Count; /// @@ -223,15 +219,7 @@ public static DrawingCanvas FromRootFrame( return FromFrame(image.Frames.RootFrame, options, clipPaths); } - /// - /// Saves the current drawing state on the state stack. - /// - /// - /// This operation stores the current canvas state by reference. - /// If the same instance is mutated after - /// , those mutations are visible when restoring. - /// - /// The save count after the state has been pushed. + /// public int Save() { this.EnsureNotDisposed(); @@ -239,16 +227,7 @@ public int Save() return this.savedStates.Count; } - /// - /// Saves the current drawing state and replaces the active state with the provided options and clip paths. - /// - /// - /// The provided instance is stored by reference. - /// Mutating it after this call mutates the active/restored state behavior. - /// - /// Drawing options for the new active state. - /// Clip paths for the new active state. - /// The save count after the previous state has been pushed. + /// public int Save(DrawingOptions options, params IPath[] clipPaths) { this.EnsureNotDisposed(); @@ -262,9 +241,7 @@ public int Save(DrawingOptions options, params IPath[] clipPaths) return this.savedStates.Count; } - /// - /// Restores the most recently saved state. - /// + /// public void Restore() { this.EnsureNotDisposed(); @@ -276,14 +253,7 @@ public void Restore() _ = this.savedStates.Pop(); } - /// - /// Restores to a specific save count. - /// - /// - /// State frames above are discarded, - /// and the last discarded frame becomes the current state. - /// - /// The save count to restore to. + /// public void RestoreTo(int saveCount) { this.EnsureNotDisposed(); @@ -295,11 +265,7 @@ public void RestoreTo(int saveCount) } } - /// - /// Creates a child canvas over a subregion in local coordinates. - /// - /// The child region in local coordinates. - /// A child canvas with local origin at (0,0). + /// public DrawingCanvas CreateRegion(Rectangle region) { this.EnsureNotDisposed(); @@ -309,10 +275,11 @@ public DrawingCanvas CreateRegion(Rectangle region) return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.ResolveState()); } - /// - /// Clears the whole canvas using the given brush and clear-style composition options. - /// - /// Brush used to shade destination pixels during clear. + /// + IDrawingCanvas IDrawingCanvas.CreateRegion(Rectangle region) + => this.CreateRegion(region); + + /// public void Clear(Brush brush) { DrawingCanvasState state = this.ResolveState(); @@ -320,11 +287,7 @@ public void Clear(Brush brush) this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush)); } - /// - /// Clears a local region using the given brush and clear-style composition options. - /// - /// Region to clear in local coordinates. - /// Brush used to shade destination pixels during clear. + /// public void Clear(Rectangle region, Brush brush) { DrawingCanvasState state = this.ResolveState(); @@ -332,11 +295,7 @@ public void Clear(Rectangle region, Brush brush) this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(region, brush)); } - /// - /// Clears a path region using the given brush and clear-style composition options. - /// - /// The path region to clear. - /// Brush used to shade destination pixels during clear. + /// public void Clear(IPath path, Brush brush) { DrawingCanvasState state = this.ResolveState(); @@ -344,26 +303,15 @@ public void Clear(IPath path, Brush brush) this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(path, brush)); } - /// - /// Fills the whole canvas using the given brush. - /// - /// Brush used to shade destination pixels. + /// public void Fill(Brush brush) => this.Fill(this.Bounds, brush); - /// - /// Fills a local region using the given brush. - /// - /// Region to fill in local coordinates. - /// Brush used to shade destination pixels. + /// public void Fill(Rectangle region, Brush brush) => this.Fill(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), brush); - /// - /// Fills all paths in a collection using the given brush and drawing options. - /// - /// Brush used to shade covered pixels. - /// Path collection to fill. + /// public void Fill(Brush brush, IPathCollection paths) { Guard.NotNull(paths, nameof(paths)); @@ -373,22 +321,14 @@ public void Fill(Brush brush, IPathCollection paths) } } - /// - /// Fills a path built by the provided builder using the given brush. - /// - /// The path builder describing the fill region. - /// Brush used to shade covered pixels. + /// public void Fill(PathBuilder pathBuilder, Brush brush) { Guard.NotNull(pathBuilder, nameof(pathBuilder)); this.Fill(pathBuilder.Build(), brush); } - /// - /// Fills a path in local coordinates using the given brush. - /// - /// The path to fill. - /// Brush used to shade covered pixels. + /// public void Fill(IPath path, Brush brush) { this.EnsureNotDisposed(); @@ -409,62 +349,33 @@ public void Fill(IPath path, Brush brush) this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); } - /// - /// Draws an arc outline using the provided pen and drawing options. - /// - /// Pen used to generate the arc outline. - /// Arc center point in local coordinates. - /// Arc radii in local coordinates. - /// Ellipse rotation in degrees. - /// Arc start angle in degrees. - /// Arc sweep angle in degrees. + /// public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) => this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); - /// - /// Draws a cubic bezier outline using the provided pen and drawing options. - /// - /// Pen used to generate the bezier outline. - /// Bezier control points. + /// public void DrawBezier(Pen pen, params PointF[] points) { Guard.NotNull(points, nameof(points)); this.Draw(pen, new Path(new CubicBezierLineSegment(points))); } - /// - /// Draws an ellipse outline using the provided pen and drawing options. - /// - /// Pen used to generate the ellipse outline. - /// Ellipse center point in local coordinates. - /// Ellipse width and height in local coordinates. + /// public void DrawEllipse(Pen pen, PointF center, SizeF size) => this.Draw(pen, new EllipsePolygon(center, size)); - /// - /// Draws a polyline outline using the provided pen and drawing options. - /// - /// Pen used to generate the line outline. - /// Polyline points. + /// public void DrawLine(Pen pen, params PointF[] points) { Guard.NotNull(points, nameof(points)); this.Draw(pen, new Path(points)); } - /// - /// Draws a rectangular outline using the provided pen and drawing options. - /// - /// Pen used to generate the rectangle outline. - /// Rectangle region to stroke. + /// public void Draw(Pen pen, Rectangle region) => this.Draw(pen, new RectangularPolygon(region.X, region.Y, region.Width, region.Height)); - /// - /// Draws all paths in a collection using the provided pen and drawing options. - /// - /// Pen used to generate outlines. - /// Path collection to stroke. + /// public void Draw(Pen pen, IPathCollection paths) { Guard.NotNull(paths, nameof(paths)); @@ -474,22 +385,14 @@ public void Draw(Pen pen, IPathCollection paths) } } - /// - /// Draws a path outline built by the provided builder using the given pen. - /// - /// Pen used to generate the outline fill path. - /// The path builder describing the path to stroke. + /// public void Draw(Pen pen, PathBuilder pathBuilder) { Guard.NotNull(pathBuilder, nameof(pathBuilder)); this.Draw(pen, pathBuilder.Build()); } - /// - /// Draws a path outline in local coordinates using the given pen. - /// - /// Pen used to generate the outline fill path. - /// The path to stroke. + /// public void Draw(Pen pen, IPath path) { this.EnsureNotDisposed(); @@ -519,13 +422,7 @@ public void Draw(Pen pen, IPath path) this.FillPathCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); } - /// - /// Draws text onto this canvas. - /// - /// The text rendering options. - /// The text to draw. - /// Optional brush used to fill glyphs. - /// Optional pen used to outline glyphs. + /// public void DrawText( RichTextOptions textOptions, string text, @@ -552,12 +449,7 @@ public void DrawText( this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); } - /// - /// Measures the advance box of the specified text. - /// - /// Text layout options. - /// The text to measure. - /// The measured advance as a rectangle in px units. + /// public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) { this.EnsureNotDisposed(); @@ -568,12 +460,7 @@ public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) return RectangleF.FromLTRB(0, 0, advance.Width, advance.Height); } - /// - /// Measures the tight bounds of the specified text. - /// - /// Text layout options. - /// The text to measure. - /// The measured bounds rectangle in px units. + /// public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text) { this.EnsureNotDisposed(); @@ -584,12 +471,7 @@ public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text) return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); } - /// - /// Measures the size of the specified text. - /// - /// Text layout options. - /// The text to measure. - /// The measured size as a rectangle in px units. + /// public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) { this.EnsureNotDisposed(); @@ -600,13 +482,7 @@ public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) return RectangleF.FromLTRB(0, 0, size.Width, size.Height); } - /// - /// Tries to measure per-character advances for the specified text. - /// - /// Text layout options. - /// The text to measure. - /// Receives per-character advance metrics in px units. - /// if all character advances were measured; otherwise . + /// public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances) { this.EnsureNotDisposed(); @@ -616,13 +492,7 @@ public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text return TextMeasurer.TryMeasureCharacterAdvances(text, textOptions, out advances); } - /// - /// Tries to measure per-character bounds for the specified text. - /// - /// Text layout options. - /// The text to measure. - /// Receives per-character bounds in px units. - /// if all character bounds were measured; otherwise . + /// public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds) { this.EnsureNotDisposed(); @@ -632,13 +502,7 @@ public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds); } - /// - /// Tries to measure per-character sizes for the specified text. - /// - /// Text layout options. - /// The text to measure. - /// Receives per-character sizes in px units. - /// if all character sizes were measured; otherwise . + /// public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes) { this.EnsureNotDisposed(); @@ -648,12 +512,7 @@ public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, o return TextMeasurer.TryMeasureCharacterSizes(text, textOptions, out sizes); } - /// - /// Counts the rendered text lines for the specified text. - /// - /// Text layout options. - /// The text to measure. - /// The number of rendered lines. + /// public int CountTextLines(RichTextOptions textOptions, string text) { this.EnsureNotDisposed(); @@ -663,12 +522,7 @@ public int CountTextLines(RichTextOptions textOptions, string text) return TextMeasurer.CountLines(text, textOptions); } - /// - /// Gets line metrics for the specified text. - /// - /// Text layout options. - /// The text to measure. - /// An array of line metrics in px units. + /// public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text) { this.EnsureNotDisposed(); @@ -678,23 +532,44 @@ public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text return TextMeasurer.GetLineMetrics(text, textOptions); } - /// - /// Draws an image source region into a destination rectangle. - /// - /// The source image. - /// The source rectangle within . - /// The destination rectangle in local canvas coordinates. - /// - /// Optional resampler used when scaling or transforming the image. Defaults to . - /// + /// + void IDrawingCanvas.DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + + if (image is Image specificImage) + { + this.DrawImageCore(specificImage, sourceRect, destinationRect, sampler, ownsSourceImage: false); + return; + } + + Image convertedImage = image.CloneAs(); + this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true); + } + + /// public void DrawImage( Image image, Rectangle sourceRect, RectangleF destinationRect, IResampler? sampler = null) + => this.DrawImageCore(image, sourceRect, destinationRect, sampler, ownsSourceImage: false); + + private void DrawImageCore( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler, + bool ownsSourceImage) { this.EnsureNotDisposed(); Guard.NotNull(image, nameof(image)); + bool disposeSourceImage = ownsSourceImage; DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; @@ -772,9 +647,20 @@ public void DrawImage( // Phase 3: Transfer temp-image ownership to deferred batch execution. if (!ReferenceEquals(brushImage, image)) { + if (disposeSourceImage) + { + image.Dispose(); + disposeSourceImage = false; + } + this.pendingImageResources.Add(brushImage); ownedImage = null; } + else if (disposeSourceImage) + { + this.pendingImageResources.Add(image); + disposeSourceImage = false; + } ImageBrush brush = new(brushImage, brushImageRegion); IPath destinationPath = new RectangularPolygon( @@ -788,6 +674,10 @@ public void DrawImage( finally { ownedImage?.Dispose(); + if (disposeSourceImage) + { + image.Dispose(); + } } } @@ -917,9 +807,7 @@ private static IPath ApplyClipPaths(IPath subjectPath, ShapeOptions shapeOptions return subjectPath.Clip(shapeOptions, clipPaths); } - /// - /// Flushes queued drawing commands to the target in submission order. - /// + /// public void Flush() { this.EnsureNotDisposed(); diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs index dab8a1f44..ddfc43baf 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs @@ -6,48 +6,42 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Represents a drawing callback executed against a . +/// Represents a drawing callback executed against a . /// -/// The pixel format. /// The drawing canvas for the current frame. -public delegate void CanvasAction(DrawingCanvas canvas) - where TPixel : unmanaged, IPixel; +public delegate void CanvasAction(IDrawingCanvas canvas); /// -/// Adds extensions that execute drawing callbacks against all frames through . +/// Adds extensions that execute drawing callbacks against all frames through . /// public static class ProcessWithCanvasExtensions { /// /// Executes for each image frame using drawing options from the current context. /// - /// The pixel format expected by the callback. /// The source image processing context. /// The drawing callback to execute for each frame. /// The to allow chaining of operations. - public static IImageProcessingContext ProcessWithCanvas( + public static IImageProcessingContext ProcessWithCanvas( this IImageProcessingContext source, - CanvasAction action) - where TPixel : unmanaged, IPixel + CanvasAction action) => source.ProcessWithCanvas(source.GetDrawingOptions(), action); /// /// Executes for each image frame using the supplied drawing options. /// - /// The pixel format expected by the callback. /// The source image processing context. /// The drawing options. /// The drawing callback to execute for each frame. /// The to allow chaining of operations. - public static IImageProcessingContext ProcessWithCanvas( + public static IImageProcessingContext ProcessWithCanvas( this IImageProcessingContext source, DrawingOptions options, - CanvasAction action) - where TPixel : unmanaged, IPixel + CanvasAction action) { Guard.NotNull(options, nameof(options)); Guard.NotNull(action, nameof(action)); - return source.ApplyProcessor(new ProcessWithCanvasProcessor(options, typeof(TPixel), action)); + return source.ApplyProcessor(new ProcessWithCanvasProcessor(options, action)); } } diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs new file mode 100644 index 000000000..5d2f4c962 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -0,0 +1,283 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents a drawing canvas over a frame target. +/// +public interface IDrawingCanvas : IDisposable +{ + /// + /// Gets the local bounds of this canvas. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the number of saved states currently on the canvas stack. + /// + public int SaveCount { get; } + + /// + /// Saves the current drawing state on the state stack. + /// + /// + /// This operation stores the current canvas state by reference. + /// If the same instance is mutated after + /// , those mutations are visible when restoring. + /// + /// The save count after the state has been pushed. + public int Save(); + + /// + /// Saves the current drawing state and replaces the active state with the provided options and clip paths. + /// + /// + /// The provided instance is stored by reference. + /// Mutating it after this call mutates the active/restored state behavior. + /// + /// Drawing options for the new active state. + /// Clip paths for the new active state. + /// The save count after the previous state has been pushed. + public int Save(DrawingOptions options, params IPath[] clipPaths); + + /// + /// Restores the most recently saved state. + /// + public void Restore(); + + /// + /// Restores to a specific save count. + /// + /// + /// State frames above are discarded, + /// and the last discarded frame becomes the current state. + /// + /// The save count to restore to. + public void RestoreTo(int saveCount); + + /// + /// Creates a child canvas over a subregion in local coordinates. + /// + /// The child region in local coordinates. + /// A child canvas with local origin at (0,0). + public IDrawingCanvas CreateRegion(Rectangle region); + + /// + /// Clears the whole canvas using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + public void Clear(Brush brush); + + /// + /// Clears a local region using the given brush and clear-style composition options. + /// + /// Region to clear in local coordinates. + /// Brush used to shade destination pixels during clear. + public void Clear(Rectangle region, Brush brush); + + /// + /// Clears a path region using the given brush and clear-style composition options. + /// + /// The path region to clear. + /// Brush used to shade destination pixels during clear. + public void Clear(IPath path, Brush brush); + + /// + /// Fills the whole canvas using the given brush. + /// + /// Brush used to shade destination pixels. + public void Fill(Brush brush); + + /// + /// Fills a local region using the given brush. + /// + /// Region to fill in local coordinates. + /// Brush used to shade destination pixels. + public void Fill(Rectangle region, Brush brush); + + /// + /// Fills all paths in a collection using the given brush and drawing options. + /// + /// Brush used to shade covered pixels. + /// Path collection to fill. + public void Fill(Brush brush, IPathCollection paths); + + /// + /// Fills a path built by the provided builder using the given brush. + /// + /// The path builder describing the fill region. + /// Brush used to shade covered pixels. + public void Fill(PathBuilder pathBuilder, Brush brush); + + /// + /// Fills a path in local coordinates using the given brush. + /// + /// The path to fill. + /// Brush used to shade covered pixels. + public void Fill(IPath path, Brush brush); + + /// + /// Draws an arc outline using the provided pen and drawing options. + /// + /// Pen used to generate the arc outline. + /// Arc center point in local coordinates. + /// Arc radii in local coordinates. + /// Ellipse rotation in degrees. + /// Arc start angle in degrees. + /// Arc sweep angle in degrees. + public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle); + + /// + /// Draws a cubic bezier outline using the provided pen and drawing options. + /// + /// Pen used to generate the bezier outline. + /// Bezier control points. + public void DrawBezier(Pen pen, params PointF[] points); + + /// + /// Draws an ellipse outline using the provided pen and drawing options. + /// + /// Pen used to generate the ellipse outline. + /// Ellipse center point in local coordinates. + /// Ellipse width and height in local coordinates. + public void DrawEllipse(Pen pen, PointF center, SizeF size); + + /// + /// Draws a polyline outline using the provided pen and drawing options. + /// + /// Pen used to generate the line outline. + /// Polyline points. + public void DrawLine(Pen pen, params PointF[] points); + + /// + /// Draws a rectangular outline using the provided pen and drawing options. + /// + /// Pen used to generate the rectangle outline. + /// Rectangle region to stroke. + public void Draw(Pen pen, Rectangle region); + + /// + /// Draws all paths in a collection using the provided pen and drawing options. + /// + /// Pen used to generate outlines. + /// Path collection to stroke. + public void Draw(Pen pen, IPathCollection paths); + + /// + /// Draws a path outline built by the provided builder using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path builder describing the path to stroke. + public void Draw(Pen pen, PathBuilder pathBuilder); + + /// + /// Draws a path outline in local coordinates using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path to stroke. + public void Draw(Pen pen, IPath path); + + /// + /// Draws text onto this canvas. + /// + /// The text rendering options. + /// The text to draw. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public void DrawText( + RichTextOptions textOptions, + string text, + Brush? brush, + Pen? pen); + + /// + /// Measures the advance box of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured advance as a rectangle in px units. + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text); + + /// + /// Measures the tight bounds of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured bounds rectangle in px units. + public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text); + + /// + /// Measures the size of the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The measured size as a rectangle in px units. + public RectangleF MeasureTextSize(RichTextOptions textOptions, string text); + + /// + /// Tries to measure per-character advances for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character advance metrics in px units. + /// if all character advances were measured; otherwise . + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances); + + /// + /// Tries to measure per-character bounds for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character bounds in px units. + /// if all character bounds were measured; otherwise . + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds); + + /// + /// Tries to measure per-character sizes for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// Receives per-character sizes in px units. + /// if all character sizes were measured; otherwise . + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes); + + /// + /// Counts the rendered text lines for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// The number of rendered lines. + public int CountTextLines(RichTextOptions textOptions, string text); + + /// + /// Gets line metrics for the specified text. + /// + /// Text layout options. + /// The text to measure. + /// An array of line metrics in px units. + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text); + + /// + /// Draws an image source region into a destination rectangle. + /// + /// The source image. + /// The source rectangle within . + /// The destination rectangle in local canvas coordinates. + /// + /// Optional resampler used when scaling or transforming the image. Defaults to . + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler = null); + + /// + /// Flushes queued drawing commands to the target in submission order. + /// + public void Flush(); +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs index a86ade0df..8e41345a6 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs @@ -14,16 +14,13 @@ public sealed class ProcessWithCanvasProcessor : IImageProcessor /// Initializes a new instance of the class. /// /// The drawing options. - /// The pixel type expected by . /// The per-frame canvas callback. - public ProcessWithCanvasProcessor(DrawingOptions options, Type pixelType, Delegate action) + public ProcessWithCanvasProcessor(DrawingOptions options, CanvasAction action) { Guard.NotNull(options, nameof(options)); - Guard.NotNull(pixelType, nameof(pixelType)); Guard.NotNull(action, nameof(action)); this.Options = options; - this.PixelType = pixelType; this.Action = action; } @@ -32,9 +29,7 @@ public ProcessWithCanvasProcessor(DrawingOptions options, Type pixelType, Delega /// public DrawingOptions Options { get; } - internal Type PixelType { get; } - - internal Delegate Action { get; } + internal CanvasAction Action { get; } /// public IImageProcessor CreatePixelSpecificProcessor( @@ -42,13 +37,5 @@ public IImageProcessor CreatePixelSpecificProcessor( Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - { - if (typeof(TPixel) != this.PixelType) - { - throw new InvalidOperationException( - $"ProcessWithCanvas expects pixel type '{this.PixelType.Name}' but the image uses '{typeof(TPixel).Name}'."); - } - - return new ProcessWithCanvasProcessor(configuration, this, source, sourceRectangle); - } + => new ProcessWithCanvasProcessor(configuration, this, source, sourceRectangle); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs index cb0d76dc6..938bd6723 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs @@ -13,7 +13,7 @@ internal sealed class ProcessWithCanvasProcessor : ImageProcessor { private readonly ProcessWithCanvasProcessor definition; - private readonly CanvasAction action; + private readonly CanvasAction action; /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ public ProcessWithCanvasProcessor( : base(configuration, source, sourceRectangle) { this.definition = definition; - this.action = (CanvasAction)definition.Action; + this.action = definition.Action; } /// diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs index 689806c87..e7d017c8b 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs @@ -4,7 +4,6 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; @@ -15,7 +14,7 @@ public class ProcessWithCanvas : BaseImageOperationsExtensionTest [Fact] public void CanvasActionDefaultOptions() { - this.operations.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Red))); + this.operations.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Red))); ProcessWithCanvasProcessor processor = this.Verify(); @@ -29,7 +28,7 @@ public void CanvasActionDefaultOptions() [Fact] public void CanvasActionWithOptions() { - this.operations.ProcessWithCanvas( + this.operations.ProcessWithCanvas( this.nonDefaultOptions, canvas => canvas.Clear(Brushes.Solid(Color.Red))); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs index 1f838f135..2d562e01b 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs @@ -15,7 +15,7 @@ public void ProcessWithCanvas_Mutate_AppliesToAllFrames() using Image image = new(24, 16); image.Frames.AddFrame(image.Frames.RootFrame); - image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.OrangeRed)))); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.OrangeRed)))); Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames.RootFrame[8, 6]); Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames[1][8, 6]); @@ -26,10 +26,10 @@ public void ProcessWithCanvas_Clone_AppliesToAllFrames_WithoutMutatingSource() { using Image source = new(24, 16); source.Frames.AddFrame(source.Frames.RootFrame); - source.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.White)))); + source.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.White)))); using Image clone = source.Clone( - ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.MediumPurple)))); + ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.MediumPurple)))); Assert.Equal(Color.White.ToPixel(), source.Frames.RootFrame[8, 6]); Assert.Equal(Color.White.ToPixel(), source.Frames[1][8, 6]); @@ -38,13 +38,28 @@ public void ProcessWithCanvas_Clone_AppliesToAllFrames_WithoutMutatingSource() } [Fact] - public void ProcessWithCanvas_WhenPixelTypeMismatch_Throws() + public void ProcessWithCanvas_Mutate_DrawImage_AppliesToAllFrames() { - using Image image = new(12, 12); + using Image image = new(24, 16); + image.Frames.AddFrame(image.Frames.RootFrame); + + using Image source = new(8, 8, Color.HotPink.ToPixel()); + + Rectangle sourceRect = new(2, 1, 4, 5); + RectangleF destinationRect = new(6, 4, 10, 6); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage(source, sourceRect, destinationRect); + })); - InvalidOperationException ex = Assert.Throws( - () => image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Black))))); + Rgba32 expectedFill = Color.HotPink.ToPixel(); + Rgba32 expectedBackground = Color.White.ToPixel(); - Assert.Contains("expects pixel type", ex.Message); + Assert.Equal(expectedFill, image.Frames.RootFrame[10, 6]); + Assert.Equal(expectedFill, image.Frames[1][10, 6]); + Assert.Equal(expectedBackground, image.Frames.RootFrame[1, 1]); + Assert.Equal(expectedBackground, image.Frames[1][1, 1]); } } From 06775a131ec55807c10a649138e152c480c09534 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 00:43:09 +1000 Subject: [PATCH 041/136] Add DrawGlyphs API and glyph rendering tests --- .../Processing/DrawingCanvas{TPixel}.cs | 70 +++++++++++++++++++ .../Processing/IDrawingCanvas.cs | 17 +++++ .../Shapes/Text/BaseGlyphBuilder.cs | 4 +- .../Processing/DrawingCanvasTests.Text.cs | 41 +++++++++++ ...hesReference_Rgba32_ColrV1-draw-glyphs.png | 3 + ...atchesReference_Rgba32_Svg-draw-glyphs.png | 3 + 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 8963050cc..af23b64a3 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -449,6 +449,76 @@ public void DrawText( this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); } + /// + public void DrawGlyphs( + Brush brush, + Pen pen, + IReadOnlyList glyphs) + { + this.EnsureNotDisposed(); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(glyphs, nameof(glyphs)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions baseOptions = state.Options; + IReadOnlyList clipPaths = state.ClipPaths; + + for (int glyphIndex = 0; glyphIndex < glyphs.Count; glyphIndex++) + { + GlyphPathCollection glyph = glyphs[glyphIndex]; + if (glyph.LayerCount == 0) + { + continue; + } + + if (glyph.LayerCount == 1) + { + this.Fill(brush, glyph.Paths); + continue; + } + + float glyphArea = glyph.Bounds.Width * glyph.Bounds.Height; + for (int layerIndex = 0; layerIndex < glyph.LayerCount; layerIndex++) + { + GlyphLayerInfo layer = glyph.Layers[layerIndex]; + if (layer.Count == 0) + { + continue; + } + + PathCollection layerPaths = glyph.GetLayerPaths(layerIndex); + DrawingOptions layerOptions = baseOptions.CloneOrReturnForRules( + layer.IntersectionRule, + layer.PixelAlphaCompositionMode, + layer.PixelColorBlendingMode); + + bool shouldFill; + if (layer.Kind is GlyphLayerKind.Decoration or GlyphLayerKind.Glyph) + { + shouldFill = true; + } + else + { + float layerArea = layerPaths.ComputeArea(); + shouldFill = layerArea > 0F && glyphArea > 0F && (layerArea / glyphArea) < 0.50F; + } + + this.ExecuteWithTemporaryState(layerOptions, clipPaths, () => + { + if (shouldFill) + { + this.Fill(brush, layerPaths); + } + else + { + this.Draw(pen, layerPaths); + } + }); + } + } + } + /// public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) { diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 5d2f4c962..770400dee 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -194,6 +195,22 @@ public void DrawText( Brush? brush, Pen? pen); + /// + /// Draws layered glyph geometry using a monochrome projection. + /// + /// + /// For painted glyph layers, the implementation uses a coverage/compactness heuristic + /// to keep one dominant background-like layer as outline-only to preserve interior definition. + /// All non-painted layers are filled. + /// + /// Brush used to fill glyph layers. + /// Pen used to outline dominant painted layers. + /// Layered glyph geometry to draw. + public void DrawGlyphs( + Brush brush, + Pen pen, + IReadOnlyList glyphs); + /// /// Measures the advance box of the specified text. /// diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index 846d7e79f..76213345a 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -439,9 +439,11 @@ void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start this.CurrentPaths.Add(path); if (this.graphemeBuilder is not null) { + // Decorations are emitted as independent paths; each layer must point + // at the path index appended for this specific decoration. this.graphemeBuilder.AddPath(path); this.graphemeBuilder.AddLayer( - startIndex: this.layerStartIndex, + startIndex: this.graphemePathCount, count: 1, paint: this.currentLayerPaint, fillRule: FillRule.NonZero, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs index 95a153851..a6d20c335 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -3,13 +3,54 @@ using System.Numerics; using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public partial class DrawingCanvasTests { + [Theory] + [WithSolidFilledImages(492, 360, nameof(Color.White), PixelTypes.Rgba32, ColorFontSupport.ColrV1)] + [WithSolidFilledImages(492, 360, nameof(Color.White), PixelTypes.Rgba32, ColorFontSupport.Svg)] + public void DrawGlyphs_EmojiFont_MatchesReference(TestImageProvider provider, ColorFontSupport support) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.NotoColorEmojiRegular, 100); + Font fallback = TestFontUtilities.GetFont(TestFonts.OpenSans, 100); + const string text = "a\U0001F628 b\U0001F605\r\nc\U0001F972 d\U0001F929"; + + RichTextOptions textOptions = new(font) + { + ColorFontSupport = support, + LineSpacing = 1.8F, + FallbackFontFamilies = [fallback.Family], + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = text.GetGraphemeCount(), + TextDecorations = TextDecorations.Strikeout | TextDecorations.Underline | TextDecorations.Overline + } + ] + }; + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, textOptions); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawGlyphs(Brushes.Solid(Color.Black), Pens.Solid(Color.Black, 2F), glyphs); + canvas.Flush(); + + target.DebugSave(provider, $"{support}-draw-glyphs", appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, $"{support}-draw-glyphs", appendSourceFileOrDescription: false); + } + [Theory] [WithBlankImage(760, 320, PixelTypes.Rgba32)] public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(TestImageProvider provider) diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png new file mode 100644 index 000000000..d65188782 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9392c829544595e4fd94f9555151d80bbd2b1b3f21651cea5c3d7a255eabaa43 +size 23306 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png new file mode 100644 index 000000000..b13d694de --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7f4076fd9e235fd64c6ac1078787b44738fafbe92dfc7ab57ae1ee4995b8d19 +size 23309 From 48d4b225eef3906cdd1e0ee427d23f3201cbab52 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 00:44:34 +1000 Subject: [PATCH 042/136] Use literal emoji in test string --- .../Processing/DrawingCanvasTests.Text.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs index a6d20c335..f41010552 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -23,7 +23,7 @@ public void DrawGlyphs_EmojiFont_MatchesReference(TestImageProvider Date: Wed, 4 Mar 2026 10:10:59 +1000 Subject: [PATCH 043/136] Add Process API, shadow fallback & WebGPU readback --- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 65 ++++- .../WebGPUDrawingBackend.Readback.cs | 227 ++++++++++++++++ .../Backends/DefaultDrawingBackend.cs | 48 +++- .../Processing/Backends/IDrawingBackend.cs | 19 ++ .../DrawingCanvasBatcher{TPixel}.cs | 13 +- .../Processing/DrawingCanvas{TPixel}.cs | 224 +++++++++++++++- .../Processing/IDrawingCanvas.cs | 25 ++ .../Backends/SkiaCoverageDrawingBackend.cs | 12 + .../Backends/WebGPUDrawingBackendTests.cs | 108 ++++++-- .../Processing/DrawingCanvasBatcherTests.cs | 16 +- .../Processing/DrawingCanvasTests.Process.cs | 244 ++++++++++++++++++ .../RasterizerDefaultsExtensionsTests.cs | 12 + .../NativeSurfaceOnlyFrame{TPixel}.cs | 38 +++ ...backCapability_MatchesReference_Rgba32.png | 3 + ...backCapability_MatchesReference_Rgba32.png | 3 + .../Process_Path_MatchesReference_Rgba32.png | 3 + 16 files changed, 1011 insertions(+), 49 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs create mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index cfbe15a45..106c70356 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -537,37 +537,76 @@ private bool TryUploadDirtyCoverageRange( Span cached = cachedOwner.Memory.Span[..source.Length]; int previousLength = cachedLength; int commonLength = Math.Min(previousLength, source.Length); + int commonAlignedLength = commonLength & ~0x3; + ReadOnlySpan sourceWords = MemoryMarshal.Cast(source[..commonAlignedLength]); + ReadOnlySpan cachedWords = MemoryMarshal.Cast(cached[..commonAlignedLength]); - int firstDifferent = 0; + // Scan forward in 32-bit words first, then finish any remaining tail bytes. + int firstDifferentWord = 0; + while (firstDifferentWord < sourceWords.Length && cachedWords[firstDifferentWord] == sourceWords[firstDifferentWord]) + { + firstDifferentWord++; + } + + int firstDifferent = firstDifferentWord * sizeof(uint); while (firstDifferent < commonLength && cached[firstDifferent] == source[firstDifferent]) { firstDifferent++; } - int uploadLength = 0; + // No upload needed when the source payload matches the cached upload exactly. if (firstDifferent < source.Length) { + // Trim unchanged suffix in reverse. Start with bytes above the aligned word boundary, + // then continue with 32-bit word comparisons. int lastDifferent = source.Length - 1; - while (lastDifferent >= firstDifferent && - lastDifferent < commonLength && - cached[lastDifferent] == source[lastDifferent]) + if (lastDifferent < commonLength) { - lastDifferent--; + while (lastDifferent >= firstDifferent && + lastDifferent >= commonAlignedLength && + cached[lastDifferent] == source[lastDifferent]) + { + lastDifferent--; + } + + int firstWordIndex = firstDifferent / sizeof(uint); + int lastWordIndex = Math.Min(lastDifferent / sizeof(uint), sourceWords.Length - 1); + while (lastWordIndex >= firstWordIndex && cachedWords[lastWordIndex] == sourceWords[lastWordIndex]) + { + lastWordIndex--; + } + + if (lastWordIndex >= firstWordIndex) + { + // End on the containing word boundary; this may include up to 3 unchanged bytes. + lastDifferent = Math.Min(lastDifferent, (lastWordIndex * sizeof(uint)) + (sizeof(uint) - 1)); + } } - uploadLength = (lastDifferent - firstDifferent) + 1; - } + int uploadLength = (lastDifferent - firstDifferent) + 1; + + // Only write the dirty range to reduce queue upload bandwidth on repeated flushes. + // QueueWriteBuffer requires 4-byte aligned offsets and sizes. + // firstDifferent/uploadLength come from byte-wise diffing, so they can land + // in the middle of a 32-bit value. Expand the upload window to 4-byte bounds. + // `& ~0x3` clears the lower 2 bits (align down to previous multiple of 4). + int uploadOffset = firstDifferent & ~0x3; + + int uploadEnd = firstDifferent + uploadLength; + + // `(x + 3) & ~0x3` rounds up to the next multiple of 4. + // Clamp afterwards so the rounded end never exceeds source length. + uploadEnd = (uploadEnd + 3) & ~0x3; + uploadEnd = Math.Min(uploadEnd, source.Length); + uploadLength = uploadEnd - uploadOffset; - // Only write the dirty range to reduce queue upload bandwidth on repeated flushes. - if (uploadLength > 0) - { fixed (byte* sourcePtr = source) { flushContext.Api.QueueWriteBuffer( flushContext.Queue, destinationBuffer, - (nuint)firstDifferent, - sourcePtr + firstDifferent, + (nuint)uploadOffset, + sourcePtr + uploadOffset, (nuint)uploadLength); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs new file mode 100644 index 000000000..564cba7a3 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -0,0 +1,227 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal sealed unsafe partial class WebGPUDrawingBackend +{ + private const int ReadbackCallbackTimeoutMilliseconds = 5000; + + /// + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target, nameof(target)); + + image = null; + + // When a CPU-backed frame is used with this backend (for example in parity tests), + // delegate to the default CPU readback implementation. + if (target.TryGetCpuRegion(out _)) + { + return this.fallbackBackend.TryReadRegion(configuration, target, sourceRectangle, out image); + } + + // Readback is only available for native WebGPU targets with valid interop handles. + if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability) || + capability.Device == 0 || + capability.Queue == 0 || + capability.TargetTexture == 0) + { + return false; + } + + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId expectedFormat) || + expectedFormat != capability.TargetFormat) + { + return false; + } + + // Convert canvas-local source coordinates to absolute native-surface coordinates. + Rectangle absoluteSource = new( + target.Bounds.X + sourceRectangle.X, + target.Bounds.Y + sourceRectangle.Y, + sourceRectangle.Width, + sourceRectangle.Height); + Rectangle surfaceBounds = new(0, 0, capability.Width, capability.Height); + Rectangle source = Rectangle.Intersect(surfaceBounds, absoluteSource); + if (source.Width <= 0 || source.Height <= 0) + { + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + Device* device = (Device*)capability.Device; + Queue* queue = (Queue*)capability.Queue; + + int pixelSizeInBytes = Unsafe.SizeOf(); + int packedRowBytes = checked(source.Width * pixelSizeInBytes); + + // WebGPU copy-to-buffer requires bytes-per-row alignment to 256 bytes. + int readbackRowBytes = Align(packedRowBytes, 256); + int packedByteCount = checked(packedRowBytes * source.Height); + ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)source.Height); + + WgpuBuffer* readbackBuffer = null; + CommandEncoder* commandEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor bufferDescriptor = new() + { + Usage = BufferUsage.CopyDst | BufferUsage.MapRead, + Size = readbackByteCount, + MappedAtCreation = false + }; + + readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor); + if (readbackBuffer is null) + { + return false; + } + + CommandEncoderDescriptor encoderDescriptor = default; + commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + // Copy only the requested source rect from the target texture into the readback buffer. + ImageCopyTexture sourceCopy = new() + { + Texture = (Texture*)capability.TargetTexture, + MipLevel = 0, + Origin = new Origin3D((uint)source.X, (uint)source.Y, 0), + Aspect = TextureAspect.All + }; + + ImageCopyBuffer destinationCopy = new() + { + Buffer = readbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = (uint)readbackRowBytes, + RowsPerImage = (uint)source.Height + } + }; + + Extent3D copySize = new((uint)source.Width, (uint)source.Height, 1); + api.CommandEncoderCopyTextureToBuffer(commandEncoder, in sourceCopy, in destinationCopy, in copySize); + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + api.QueueSubmit(queue, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + api.CommandEncoderRelease(commandEncoder); + commandEncoder = null; + + // Map the GPU buffer and wait for completion before reading host-visible bytes. + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim mapReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userData) + { + _ = userData; + mapStatus = status; + mapReady.Set(); + } + + using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null); + if (!WaitForMapSignal(lease.WgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + { + return false; + } + + void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount); + if (mapped is null) + { + api.BufferUnmap(readbackBuffer); + return false; + } + + try + { + ReadOnlySpan readback = new(mapped, checked((int)readbackByteCount)); + byte[] packed = new byte[packedByteCount]; + Span packedSpan = packed; + + // Strip WebGPU row padding so Image.LoadPixelData receives tightly packed rows. + for (int y = 0; y < source.Height; y++) + { + readback + .Slice(y * readbackRowBytes, packedRowBytes) + .CopyTo(packedSpan.Slice(y * packedRowBytes, packedRowBytes)); + } + + image = Image.LoadPixelData(configuration, packed, source.Width, source.Height); + return true; + } + finally + { + api.BufferUnmap(readbackBuffer); + } + } + finally + { + if (commandBuffer is not null) + { + api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + api.CommandEncoderRelease(commandEncoder); + } + + if (readbackBuffer is not null) + { + api.BufferRelease(readbackBuffer); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Align(int value, int alignment) + => ((value + alignment - 1) / alignment) * alignment; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WaitForMapSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + { + if (extension is null) + { + return signal.Wait(ReadbackCallbackTimeoutMilliseconds); + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < ReadbackCallbackTimeoutMilliseconds) + { + _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + } + + return signal.IsSet; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index fb6ccfa68..60d7a2599 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using System.Numerics; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; @@ -105,6 +106,47 @@ public void FlushCompositions( } } + /// + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + // CPU backend readback is available only when the target exposes CPU pixels. + if (!target.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + image = null; + return false; + } + + // Clamp the request to the target region to avoid out-of-range row slicing. + Rectangle clipped = Rectangle.Intersect( + new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height), + sourceRectangle); + + if (clipped.Width <= 0 || clipped.Height <= 0) + { + image = null; + return false; + } + + // Build a tightly packed temporary image for downstream processing operations. + image = new(configuration, clipped.Width, clipped.Height); + Buffer2D destination = image.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < clipped.Height; y++) + { + sourceRegion.DangerousGetRowSpan(clipped.Y + y) + .Slice(clipped.X, clipped.Width) + .CopyTo(destination.DangerousGetRowSpan(y)); + } + + return true; + } + /// /// Executes one prepared batch on the CPU. /// @@ -129,7 +171,11 @@ internal void FlushPreparedBatch( return; } - _ = target.TryGetCpuRegion(out Buffer2DRegion destinationFrame); + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets."); + } + CompositionCoverageDefinition definition = compositionBatch.Definition; using Buffer2D coverageMap = this.CreateCoverageMap(definition, configuration.MemoryAllocator); diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 5ccec307d..438657502 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -56,4 +57,22 @@ public void FlushCompositions( ICanvasFrame target, CompositionScene compositionScene) where TPixel : unmanaged, IPixel; + + /// + /// Attempts to read source pixels from the target into a temporary image. + /// + /// The destination pixel format. + /// The active processing configuration. + /// The target frame. + /// Source rectangle in target-local coordinates. + /// + /// When this method returns , receives a newly allocated source image. + /// + /// when readback succeeds; otherwise . + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index ee6743c58..c33021138 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -21,6 +21,7 @@ internal sealed class DrawingCanvasBatcher private readonly IDrawingBackend backend; private readonly ICanvasFrame targetFrame; private readonly List commands = []; + private DrawingCanvasBatcher? mirrorBatcher; internal DrawingCanvasBatcher( Configuration configuration, @@ -37,7 +38,17 @@ internal DrawingCanvasBatcher( /// /// The command to queue. public void AddComposition(in CompositionCommand composition) - => this.commands.Add(composition); + { + this.commands.Add(composition); + this.mirrorBatcher?.commands.Add(composition); + } + + /// + /// Sets an optional mirror batcher that receives the same queued commands. + /// + /// The mirror batcher, or to disable mirroring. + public void SetMirror(DrawingCanvasBatcher? mirrorBatcher) + => this.mirrorBatcher = mirrorBatcher; /// /// Flushes queued commands to the backend as one scene packet, preserving submission order. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index af23b64a3..55678d28c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1000 // Do not declare static members on generic types +using System.Diagnostics.CodeAnalysis; using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; @@ -42,6 +43,11 @@ public sealed class DrawingCanvas : IDrawingCanvas /// private readonly DrawingCanvasBatcher batcher; + /// + /// Optional CPU shadow fallback used when target readback is unavailable. + /// + private readonly ShadowFallbackState? shadowFallback; + /// /// Temporary image resources that must stay alive until queued commands are flushed. /// @@ -108,7 +114,8 @@ internal DrawingCanvas( backend, targetFrame, new DrawingCanvasBatcher(configuration, backend, targetFrame), - new DrawingCanvasState(options, clipPaths)) + new DrawingCanvasState(options, clipPaths), + CreateShadowFallbackIfNeeded(configuration, targetFrame)) { } @@ -121,12 +128,16 @@ internal DrawingCanvas( /// The destination frame. /// The command batcher used for deferred composition. /// The default state used when no scoped state is active. + /// Optional shared shadow fallback state. + /// Whether to increment the shared shadow fallback reference count. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, DrawingCanvasBatcher batcher, - DrawingCanvasState defaultState) + DrawingCanvasState defaultState, + ShadowFallbackState? shadowFallback = null, + bool addShadowReference = false) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); @@ -143,6 +154,13 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; + this.shadowFallback = shadowFallback; + if (addShadowReference) + { + this.shadowFallback?.AddReference(); + } + + this.batcher.SetMirror(this.shadowFallback?.Batcher); // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); @@ -272,7 +290,14 @@ public DrawingCanvas CreateRegion(Rectangle region) Rectangle clipped = Rectangle.Intersect(this.Bounds, region); ICanvasFrame childFrame = new CanvasRegionFrame(this.targetFrame, clipped); - return new DrawingCanvas(this.configuration, this.backend, childFrame, this.batcher, this.ResolveState()); + return new DrawingCanvas( + this.configuration, + this.backend, + childFrame, + this.batcher, + this.ResolveState(), + this.shadowFallback, + addShadowReference: true); } /// @@ -349,6 +374,62 @@ public void Fill(IPath path, Brush brush) this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); } + /// + public void Process(Rectangle region, Action operation) + => this.Process(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), operation); + + /// + public void Process(PathBuilder pathBuilder, Action operation) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Process(pathBuilder.Build(), operation); + } + + /// + public void Process(IPath path, Action operation) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(operation, nameof(operation)); + + // This operation samples the current destination state. Flush queued commands first + // so readback observes strict draw-order semantics. + this.Flush(); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + IPath closed = path.AsClosedPath(); + IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity + ? closed + : closed.Transform(effectiveOptions.Transform); + transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); + + Rectangle sourceRect = ToConservativeBounds(transformedPath.Bounds); + sourceRect = Rectangle.Intersect(this.Bounds, sourceRect); + if (sourceRect.Width <= 0 || sourceRect.Height <= 0) + { + return; + } + + // Defensive guard: built-in backends should provide either direct readback (CPU/backed surface) + // or shadow fallback, but custom/inconsistent backend+target combinations can still fail both paths. + if (!this.TryCreateProcessSourceImage(sourceRect, out Image? sourceImage)) + { + throw new NotSupportedException("Canvas process operations require either CPU pixels, backend readback support, or shadow fallback."); + } + + sourceImage.Mutate(operation); + + Point brushOffset = new( + sourceRect.X - (int)MathF.Floor(transformedPath.Bounds.Left), + sourceRect.Y - (int)MathF.Floor(transformedPath.Bounds.Top)); + ImageBrush brush = new(sourceImage, sourceImage.Bounds, brushOffset); + + this.pendingImageResources.Add(sourceImage); + this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); + } + /// public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) => this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); @@ -860,6 +941,29 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList + /// Attempts to create a source image for process-in-path operations. + /// + /// Source rectangle in local canvas coordinates. + /// The readback image when available. + /// when source pixels were resolved. + private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image? sourceImage) + { + if (this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage)) + { + return true; + } + + if (this.shadowFallback is not null) + { + sourceImage = this.shadowFallback.CloneRegion(sourceRect, this.configuration); + return true; + } + + sourceImage = null; + return false; + } + /// /// Applies all clip paths to a subject path using the provided shape options. /// @@ -887,6 +991,7 @@ public void Flush() } finally { + this.shadowFallback?.Flush(); this.DisposePendingImageResources(); } } @@ -905,8 +1010,16 @@ public void Dispose() } finally { - this.DisposePendingImageResources(); - this.isDisposed = true; + try + { + this.shadowFallback?.Flush(); + } + finally + { + this.DisposePendingImageResources(); + this.shadowFallback?.Release(); + this.isDisposed = true; + } } } @@ -1015,6 +1128,42 @@ private CompositionCommand CreateCompositionCommand( definitionKeyCache); } + /// + /// Clones a rectangle from a CPU region into a new image. + /// + /// The source rectangle in local region coordinates. + /// The source CPU region. + /// The processing configuration. + /// A newly allocated image containing copied pixels from . + private static Image CloneRegionFromBuffer( + Rectangle sourceRect, + Buffer2DRegion sourceRegion, + Configuration configuration) + { + Image image = new(configuration, sourceRect.Width, sourceRect.Height); + Buffer2D destination = image.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < sourceRect.Height; y++) + { + sourceRegion.DangerousGetRowSpan(sourceRect.Y + y) + .Slice(sourceRect.X, sourceRect.Width) + .CopyTo(destination.DangerousGetRowSpan(y)); + } + + return image; + } + + /// + /// Converts floating bounds to a conservative integer rectangle using floor/ceiling. + /// + /// The floating bounds to convert. + /// A rectangle covering the full floating bounds extent. + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + /// /// Creates resize options used for image drawing operations. /// @@ -1167,4 +1316,69 @@ private void DisposePendingImageResources() this.pendingImageResources.Clear(); } + + /// + /// Creates a shadow fallback state for non-CPU frame targets. + /// + /// The active processing configuration. + /// The canvas target frame. + /// A shadow fallback state when needed; otherwise . + private static ShadowFallbackState? CreateShadowFallbackIfNeeded( + Configuration configuration, + ICanvasFrame targetFrame) + { + bool hasCpuRegion = targetFrame.TryGetCpuRegion(out _); + bool hasNativeSurface = targetFrame.TryGetNativeSurface(out _); + if (hasCpuRegion || !hasNativeSurface) + { + return null; + } + + Image shadowImage = new(configuration, targetFrame.Bounds.Width, targetFrame.Bounds.Height); + Buffer2DRegion shadowRegion = new(shadowImage.Frames.RootFrame.PixelBuffer, targetFrame.Bounds); + ICanvasFrame shadowFrame = new CpuCanvasFrame(shadowRegion); + DrawingCanvasBatcher shadowBatcher = new(configuration, DefaultDrawingBackend.Instance, shadowFrame); + return new ShadowFallbackState(shadowImage, shadowBatcher); + } + + /// + /// Shared CPU shadow fallback state. + /// + private sealed class ShadowFallbackState + { + private int referenceCount = 1; + + public ShadowFallbackState(Image image, DrawingCanvasBatcher batcher) + { + this.Image = image; + this.Batcher = batcher; + } + + public Image Image { get; } + + public DrawingCanvasBatcher Batcher { get; } + + public void AddReference() + => this.referenceCount++; + + public void Release() + { + this.referenceCount--; + if (this.referenceCount > 0) + { + return; + } + + this.Image.Dispose(); + } + + public void Flush() + => this.Batcher.FlushCompositions(); + + public Image CloneRegion(Rectangle sourceRect, Configuration configuration) + { + Buffer2DRegion sourceRegion = new(this.Image.Frames.RootFrame.PixelBuffer, this.Image.Bounds); + return CloneRegionFromBuffer(sourceRect, sourceRegion, configuration); + } + } } diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 770400dee..8f85739e5 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -3,6 +3,7 @@ using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -121,6 +122,30 @@ public interface IDrawingCanvas : IDisposable /// Brush used to shade covered pixels. public void Fill(IPath path, Brush brush); + /// + /// Applies an image-processing operation to a local region. + /// + /// The local region to process. + /// The image-processing operation to apply to the region. + public void Process(Rectangle region, Action operation); + + /// + /// Applies an image-processing operation to a region described by a path builder. + /// + /// The path builder describing the region to process. + /// The image-processing operation to apply to the region. + public void Process(PathBuilder pathBuilder, Action operation); + + /// + /// Applies an image-processing operation to a path region. + /// + /// + /// The operation is constrained to the path bounds and then composited back using an . + /// + /// The path region to process. + /// The image-processing operation to apply to the region. + public void Process(IPath path, Action operation); + /// /// Draws an arc outline using the provided pen and drawing options. /// diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 0ccca3407..67cdb6376 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; @@ -99,6 +100,17 @@ public void FlushCompositions( } } + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + image = null; + return false; + } + public DrawingCoverageHandle PrepareCoverage( IPath path, in RasterizerOptions rasterizerOptions, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 396fac3c0..d944ca614 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -4,6 +4,7 @@ using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -486,6 +487,44 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_WithWebGPUBackend_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + void DrawAction(DrawingCanvas canvas) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + (Action>)DrawAction); + + DebugSaveBackendTriplet(provider, "Process", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + } + [Theory] [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) @@ -693,6 +732,47 @@ private static void RenderWithDefaultBackend(Image image, Drawin canvas.Flush(); } + private static EllipsePolygon CreateBlurEllipsePath() + => new(new PointF(55, 40), new SizeF(110, 80)); + + private static void DrawProcessScenario(DrawingCanvas canvas) + where TPixel : unmanaged, IPixel + { + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + } + + private static IPath CreatePixelateTrianglePath() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(110, 80, 220, 80); + pathBuilder.AddLine(220, 80, 165, 160); + pathBuilder.AddLine(165, 160, 110, 80); + pathBuilder.CloseAllFigures(); + return pathBuilder.Build(); + } + private static void RenderWithCpuRegionWebGpuBackend( Image image, WebGPUDrawingBackend backend, @@ -713,7 +793,7 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( WebGPUDrawingBackend backend, DrawingOptions options, Action> drawAction, - Image? initialImage = null) + Image initialImage = null) where TPixel : unmanaged, IPixel { Assert.True( @@ -865,30 +945,4 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) private static Buffer2DRegion GetFrameRegion(Image image) where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); - - private sealed class NativeSurfaceOnlyFrame : ICanvasFrame - where TPixel : unmanaged, IPixel - { - private readonly NativeSurface surface; - - public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) - { - this.Bounds = bounds; - this.surface = surface; - } - - public Rectangle Bounds { get; } - - public bool TryGetCpuRegion(out Buffer2DRegion region) - { - region = default; - return false; - } - - public bool TryGetNativeSurface(out NativeSurface surface) - { - surface = this.surface; - return true; - } - } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index f092b14da..81c365489 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; @@ -22,7 +23,7 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() IPath path = new RectangularPolygon(4, 6, 18, 12); DrawingOptions options = new(); - using DrawingCanvas canvas = new(configuration, new CpuCanvasFrame(region), options); + using DrawingCanvas canvas = new(configuration, region, options); Brush brushA = Brushes.Solid(Color.Red); Brush brushB = Brushes.Solid(Color.Blue); @@ -53,7 +54,7 @@ public void Flush_WhenAnyBrushUnsupported_DisablesSharedFlushId() IPath pathA = new RectangularPolygon(2, 2, 12, 12); IPath pathB = new RectangularPolygon(18, 18, 12, 12); DrawingOptions options = new(); - using DrawingCanvas canvas = new(configuration, new CpuCanvasFrame(region), options); + using DrawingCanvas canvas = new(configuration, region, options); canvas.Fill(pathA, Brushes.Solid(Color.Red)); canvas.Fill(pathB, Brushes.Horizontal(Color.Blue)); @@ -115,5 +116,16 @@ public void FlushCompositions( this.HasBatch = true; this.Batches.AddRange(batches); } + + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + image = null; + return false; + } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs new file mode 100644 index 000000000..a8bc7d8d9 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -0,0 +1,244 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_Path_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + + Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); + CpuCanvasFrame proxyFrame = new(targetRegion); + MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame, target); + + NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); + Configuration configuration = provider.Configuration.Clone(); + configuration.SetDrawingBackend(mirroringBackend); + + using (DrawingCanvas canvas = new( + configuration, + new NativeSurfaceOnlyFrame(target.Bounds, nativeSurface), + new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + Assert.True(mirroringBackend.ReadbackCallCount > 0); + Assert.Same(configuration, mirroringBackend.LastReadbackConfiguration); + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + + Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); + CpuCanvasFrame proxyFrame = new(targetRegion); + MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame); + NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); + Configuration configuration = provider.Configuration.Clone(); + configuration.SetDrawingBackend(mirroringBackend); + + using (DrawingCanvas canvas = new( + configuration, + new NativeSurfaceOnlyFrame(target.Bounds, nativeSurface), + new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Fact] + public void Process_UsesCanvasConfigurationForOperationContext() + { + Configuration configuration = Configuration.Default.Clone(); + using Image target = new(configuration, 48, 36); + Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); + using DrawingCanvas canvas = new(configuration, targetRegion, new DrawingOptions()); + + bool callbackInvoked = false; + bool sameConfiguration = false; + + canvas.Fill(Brushes.Solid(Color.CornflowerBlue)); + canvas.Process(new Rectangle(8, 6, 28, 20), ctx => + { + callbackInvoked = true; + sameConfiguration = ReferenceEquals(configuration, ctx.Configuration); + ctx.GaussianBlur(2F); + }); + canvas.Flush(); + + Assert.True(callbackInvoked); + Assert.True(sameConfiguration); + } + + private static void DrawProcessScenario(IDrawingCanvas canvas) + { + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + } + + private static EllipsePolygon CreateBlurEllipsePath() + => new(new PointF(55, 40), new SizeF(110, 80)); + + private static IPath CreatePixelateTrianglePath() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(110, 80, 220, 80); + pathBuilder.AddLine(220, 80, 165, 160); + pathBuilder.AddLine(165, 160, 110, 80); + pathBuilder.CloseAllFigures(); + return pathBuilder.Build(); + } + + /// + /// Test backend that mirrors composition output into a CPU frame and optionally serves readback + /// from a backing image so Process-path tests can exercise both readback and shadow-fallback flows. + /// + private sealed class MirroringCpuReadbackTestBackend : IDrawingBackend + where TPixel : unmanaged, IPixel + { + private readonly ICanvasFrame proxyFrame; + private readonly Image? readbackSource; + + public MirroringCpuReadbackTestBackend(ICanvasFrame proxyFrame, Image? readbackSource = null) + { + this.proxyFrame = proxyFrame; + this.readbackSource = readbackSource; + } + + public int ReadbackCallCount { get; private set; } + + public Configuration? LastReadbackConfiguration { get; private set; } + + public void FillPath( + ICanvasFrame target, + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + DrawingCanvasBatcher batcher) + where TTargetPixel : unmanaged, IPixel + => batcher.AddComposition( + CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); + + public bool IsCompositionBrushSupported(Brush brush) + where TTargetPixel : unmanaged, IPixel + => true; + + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TTargetPixel : unmanaged, IPixel + { + if (this.proxyFrame is not ICanvasFrame typedProxyFrame) + { + throw new NotSupportedException("Mirroring test backend pixel format mismatch."); + } + + DefaultDrawingBackend.Instance.FlushCompositions(configuration, typedProxyFrame, compositionScene); + } + + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + out Image? image) + where TTargetPixel : unmanaged, IPixel + { + this.LastReadbackConfiguration = configuration; + + if (this.readbackSource is null) + { + image = null; + return false; + } + + this.ReadbackCallCount++; + + Rectangle clipped = Rectangle.Intersect(this.readbackSource.Bounds, sourceRectangle); + if (clipped.Width <= 0 || clipped.Height <= 0) + { + image = null; + return false; + } + + using Image cropped = this.readbackSource.Clone(ctx => ctx.Crop(clipped)); + image = cropped.CloneAs(); + return true; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index c6e83feb7..88c9f54e8 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; @@ -131,5 +132,16 @@ public void FlushCompositions( where TPixel : unmanaged, IPixel { } + + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + [NotNullWhen(true)] out Image? image) + where TPixel : unmanaged, IPixel + { + image = null; + return false; + } } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs new file mode 100644 index 000000000..48a94aa69 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + +/// +/// Test frame wrapper that exposes only a native surface. +/// +/// The pixel format. +internal sealed class NativeSurfaceOnlyFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly NativeSurface surface; + + public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) + { + this.Bounds = bounds; + this.surface = surface; + } + + public Rectangle Bounds { get; } + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + public bool TryGetNativeSurface(out NativeSurface surface) + { + surface = this.surface; + return true; + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png new file mode 100644 index 000000000..6a980231d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d +size 19991 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png new file mode 100644 index 000000000..6a980231d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d +size 19991 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png new file mode 100644 index 000000000..6a980231d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d +size 19991 From e450b85117538d635d84ca4931f2bd612a98a182 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 10:53:38 +1000 Subject: [PATCH 044/136] Migrate clear brush tests to canvas API --- .../Drawing/ClearSolidBrushTests.cs | 158 ------------------ .../ProcessWithDrawingCanvasTests.Clear.cs | 158 ++++++++++++++++++ .../ProcessWithDrawingCanvasTests.cs | 9 + 3 files changed, 167 insertions(+), 158 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs deleted file mode 100644 index cc2fc17f9..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class ClearSolidBrushTests -{ - [Theory] - [WithBlankImage(1, 1, PixelTypes.Rgba32)] - [WithBlankImage(7, 4, PixelTypes.Rgba32)] - [WithBlankImage(16, 7, PixelTypes.Rgba32)] - [WithBlankImage(33, 32, PixelTypes.Rgba32)] - [WithBlankImage(400, 500, PixelTypes.Rgba32)] - public void DoesNotDependOnSize(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Clear(color)); - - image.DebugSave(provider, appendPixelTypeToFileName: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Clear(color)); - - image.DebugSave(provider, appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - image.Mutate(c => c.Clear(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void ClearAlwaysOverridesPreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - color = color.WithAlpha(0.5f); - - image.Mutate(c => c.Clear(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTest(c => c.Clear(color, region), testDetails, ImageComparer.Exact); - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage( - TestImageProvider provider, - int x0, - int y0, - int w, - int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTestOnWrappedMemoryImage( - c => c.Clear(color, region), - testDetails, - ImageComparer.Exact, - useReferenceOutputFrom: nameof(this.FillRegion)); - } - - public static readonly TheoryData BlendData = - new() - { - { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - }; -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs new file mode 100644 index 000000000..6d4c9dcc4 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBlankImage(1, 1, PixelTypes.Rgba32)] + [WithBlankImage(7, 4, PixelTypes.Rgba32)] + [WithBlankImage(16, 7, PixelTypes.Rgba32)] + [WithBlankImage(33, 32, PixelTypes.Rgba32)] + [WithBlankImage(400, 500, PixelTypes.Rgba32)] + public void DoesNotDependOnSize(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave(provider, appendPixelTypeToFileName: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] + public void DoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void WhenColorIsOpaque_OverridePreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void ClearAlwaysOverridesPreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName).WithAlpha(0.5F); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color clearColor = Color.Blue; + Color backgroundColor = Color.Red; + Rectangle region = new(x0, y0, w, h); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(region, Brushes.Solid(clearColor)))); + + image.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); + AssertRegionFill(image, region, clearColor, backgroundColor); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillRegion_WorksOnWrappedMemoryImage( + TestImageProvider provider, + int x0, + int y0, + int w, + int h) + where TPixel : unmanaged, IPixel + { + using Image source = provider.GetImage(); + Assert.True(source.DangerousTryGetSinglePixelMemory(out Memory sourcePixels)); + TestMemoryManager memoryManager = TestMemoryManager.CreateAsCopyOf(sourcePixels.Span); + using Image wrapped = Image.WrapMemory(memoryManager.Memory, source.Width, source.Height); + + Color clearColor = Color.Blue; + Color backgroundColor = Color.Red; + Rectangle region = new(x0, y0, w, h); + DrawingOptions options = new(); + + wrapped.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(region, Brushes.Solid(clearColor)))); + + wrapped.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); + AssertRegionFill(wrapped, region, clearColor, backgroundColor); + } + + private static void AssertRegionFill( + Image image, + Rectangle region, + Color inside, + Color outside) + where TPixel : unmanaged, IPixel + { + TPixel insidePixel = inside.ToPixel(); + TPixel outsidePixel = outside.ToPixel(); + Buffer2D buffer = image.Frames.RootFrame.PixelBuffer; + + for (int y = 0; y < image.Height; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < image.Width; x++) + { + TPixel expected = region.Contains(x, y) ? insidePixel : outsidePixel; + Assert.Equal(expected, row[x]); + } + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs new file mode 100644 index 000000000..87de3c0a5 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +[GroupOutput("Drawing")] +public partial class ProcessWithDrawingCanvasTests +{ +} From d39098d7cd0a54207f7d33c76a78dc2bbc188b21 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 11:03:12 +1000 Subject: [PATCH 045/136] Move Bezier draw tests to ProcessWithDrawingCanvas --- .../Drawing/DrawBezierTests.cs | 46 ---------------- .../ProcessWithDrawingCanvasTests.Clear.cs | 13 +++-- ...rocessWithDrawingCanvasTests.Primitives.cs | 54 +++++++++++++++++++ .../DrawBeziers_HotPink_A150_T5.png | 0 .../DrawBeziers_HotPink_A255_T5.png | 0 .../DrawBeziers_Red_A255_T3.png | 0 .../DrawBeziers_White_A255_T1.5.png | 0 .../DrawBeziers_White_A255_T15.png | 0 8 files changed, 60 insertions(+), 53 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs rename tests/Images/ReferenceOutput/Drawing/{DrawBezierTests => ProcessWithDrawingCanvasTests}/DrawBeziers_HotPink_A150_T5.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawBezierTests => ProcessWithDrawingCanvasTests}/DrawBeziers_HotPink_A255_T5.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawBezierTests => ProcessWithDrawingCanvasTests}/DrawBeziers_Red_A255_T3.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawBezierTests => ProcessWithDrawingCanvasTests}/DrawBeziers_White_A255_T1.5.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawBezierTests => ProcessWithDrawingCanvasTests}/DrawBeziers_White_A255_T15.png (100%) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs deleted file mode 100644 index 550590187..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawBezierTests -{ - public static readonly TheoryData DrawPathData - = new() - { - { "White", 255, 1.5f }, - { "Red", 255, 3 }, - { "HotPink", 255, 5 }, - { "HotPink", 150, 5 }, - { "White", 255, 15 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(DrawPathData), 300, 450, "Blue", PixelTypes.Rgba32)] - public void DrawBeziers(TestImageProvider provider, string colorName, byte alpha, float thickness) - where TPixel : unmanaged, IPixel - { - PointF[] points = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); - - FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; - - provider.RunValidatingProcessorTest( - x => x.DrawBeziers(color, 5f, points), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs index 6d4c9dcc4..95d55b8c3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -17,7 +16,7 @@ public partial class ProcessWithDrawingCanvasTests [WithBlankImage(16, 7, PixelTypes.Rgba32)] [WithBlankImage(33, 32, PixelTypes.Rgba32)] [WithBlankImage(400, 500, PixelTypes.Rgba32)] - public void DoesNotDependOnSize(TestImageProvider provider) + public void Clear_DoesNotDependOnSize(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -32,7 +31,7 @@ public void DoesNotDependOnSize(TestImageProvider provider) [Theory] [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) + public void Clear_DoesNotDependOnSinglePixelType(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -48,7 +47,7 @@ public void DoesNotDependOnSinglePixelType(TestImageProvider pro [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor( + public void Clear_WhenColorIsOpaque_OverridePreviousColor( TestImageProvider provider, string newColorName) where TPixel : unmanaged, IPixel @@ -70,7 +69,7 @@ public void WhenColorIsOpaque_OverridePreviousColor( [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void ClearAlwaysOverridesPreviousColor( + public void Clear_AlwaysOverridesPreviousColor( TestImageProvider provider, string newColorName) where TPixel : unmanaged, IPixel @@ -92,7 +91,7 @@ public void ClearAlwaysOverridesPreviousColor( [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) + public void Clear_Region(TestImageProvider provider, int x0, int y0, int w, int h) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); @@ -110,7 +109,7 @@ public void FillRegion(TestImageProvider provider, int x0, int y [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage( + public void Clear_Region_WorksOnWrappedMemoryImage( TestImageProvider provider, int x0, int y0, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs new file mode 100644 index 000000000..7838042d1 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static readonly TheoryData DrawBezierData = + new() + { + { "White", 255, 1.5F }, + { "Red", 255, 3F }, + { "HotPink", 255, 5F }, + { "HotPink", 150, 5F }, + { "White", 255, 15F }, + }; + + [Theory] + [WithSolidFilledImages(nameof(DrawBezierData), 300, 450, "Blue", PixelTypes.Rgba32)] + public void DrawBeziers(TestImageProvider provider, string colorName, byte alpha, float thickness) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); + FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawBezier(Pens.Solid(color, 5F), points))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png From 1d87102e355827a609d5ba078e839ea7c23874c9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 11:06:38 +1000 Subject: [PATCH 046/136] Move DrawLines tests --- .../Drawing/DrawLinesTests.cs | 202 ------------------ ...rocessWithDrawingCanvasTests.Primitives.cs | 194 +++++++++++++++++ .../DrawLinesInvalidPoints_Rgba32_T(1).png | 0 ...sInvalidPoints_Rgba32_T(1)_NoAntialias.png | 0 .../DrawLinesInvalidPoints_Rgba32_T(5).png | 0 ...sInvalidPoints_Rgba32_T(5)_NoAntialias.png | 0 ...Dot_Rgba32_Black_A(1)_T(5)_NoAntialias.png | 0 ...ot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png | 0 ...ash_Rgba32_White_A(1)_T(5)_NoAntialias.png | 0 ...gba32_LightGreen_A(1)_T(5)_NoAntialias.png | 0 ...nes_EndCapButt_Rgba32_Yellow_A(1)_T(5).png | 0 ...es_EndCapRound_Rgba32_Yellow_A(1)_T(5).png | 0 ...s_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png | 0 ...intStyleMiter_Rgba32_Yellow_A(1)_T(10).png | 0 ...intStyleRound_Rgba32_Yellow_A(1)_T(10).png | 0 ...ntStyleSquare_Rgba32_Yellow_A(1)_T(10).png | 0 ...awLines_Simple_Bgr24_Yellow_A(1)_T(10).png | 0 ...Lines_Simple_Rgba32_White_A(0.6)_T(10).png | 0 ...wLines_Simple_Rgba32_White_A(1)_T(2.5).png | 0 ...ple_Rgba32_White_A(1)_T(5)_NoAntialias.png | 0 20 files changed, 194 insertions(+), 202 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLinesInvalidPoints_Rgba32_T(1).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLinesInvalidPoints_Rgba32_T(5).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawLinesTests => ProcessWithDrawingCanvasTests}/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png (100%) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs deleted file mode 100644 index b2ba8752b..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawLinesTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] - public void DrawLines_Simple(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1f, true)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5f, true)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1f, false)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5f, false)] - public void DrawLinesInvalidPoints(TestImageProvider provider, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - SolidPen pen = new(Color.Black, thickness); - PointF[] path = [new Vector2(15f, 15f), new Vector2(15f, 15f)]; - - GraphicsOptions options = new() - { - Antialias = antialias - }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawLine(pen, path), - outputDetails, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - public void DrawLines_Dash(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.Dash(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1f, 5, false)] - public void DrawLines_Dot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.Dot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, false)] - public void DrawLines_DashDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.DashDot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Black", 1f, 5, false)] - public void DrawLines_DashDotDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.DashDotDot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Round }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapButt(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Square }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleMiter(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - private static void DrawLinesImpl( - TestImageProvider provider, - string colorName, - float alpha, - float thickness, - bool antialias, - Pen pen) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = [new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300)]; - - GraphicsOptions options = new() - { Antialias = antialias }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawLine(pen, simplePath), - outputDetails, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 7838042d1..6041efac3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -51,4 +52,197 @@ public void DrawBeziers(TestImageProvider provider, string color appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1F, 10F, true)] + public void DrawLines_Simple(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1F, true)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5F, true)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1F, false)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5F, false)] + public void DrawLinesInvalidPoints(TestImageProvider provider, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + SolidPen pen = new(Color.Black, thickness); + PointF[] path = [new Vector2(15F, 15F), new Vector2(15F, 15F)]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawLine(pen, path))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + public void DrawLines_Dash(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.Dash(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1F, 5F, false)] + public void DrawLines_Dot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.Dot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, false)] + public void DrawLines_DashDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.DashDot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Black", 1F, 5F, false)] + public void DrawLines_DashDotDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.DashDotDot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Round }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapButt(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Square }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleMiter(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + private static void DrawLinesImpl( + TestImageProvider provider, + string colorName, + float alpha, + float thickness, + bool antialias, + Pen pen) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = [new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300)]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawLine(pen, simplePath))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } } diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png From 0bfb3c0486e75fc6c97d2e3110648f63edf9a55f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 11:10:21 +1000 Subject: [PATCH 047/136] Mode DrawComplexPolygonTests --- .../Drawing/DrawComplexPolygonTests.cs | 65 ------------------- ...rocessWithDrawingCanvasTests.Primitives.cs | 60 +++++++++++++++++ .../DrawComplexPolygon.png | 3 - .../DrawComplexPolygon__Dashed.png | 3 - .../DrawComplexPolygon__Overlap.png | 3 - .../DrawComplexPolygon__Transparent.png | 3 - .../DrawComplexPolygon.png | 3 + .../DrawComplexPolygon__Dashed.png | 3 + .../DrawComplexPolygon__Overlap.png | 3 + .../DrawComplexPolygon__Transparent.png | 3 + 10 files changed, 72 insertions(+), 77 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs deleted file mode 100644 index 34026ab54..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawComplexPolygonTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, true, false, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, true, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, true)] - public void DrawComplexPolygon(TestImageProvider provider, bool overlap, bool transparent, bool dashed) - where TPixel : unmanaged, IPixel - { - Polygon simplePath = new(new LinearLineSegment( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300))); - - Polygon hole1 = new(new LinearLineSegment( - new Vector2(37, 85), - overlap ? new Vector2(130, 40) : new Vector2(93, 85), - new Vector2(65, 137))); - - IPath clipped = simplePath.Clip(hole1); - - Color color = Color.White; - if (transparent) - { - color = color.WithAlpha(150 / 255F); - } - - string testDetails = string.Empty; - if (overlap) - { - testDetails += "_Overlap"; - } - - if (transparent) - { - testDetails += "_Transparent"; - } - - if (dashed) - { - testDetails += "_Dashed"; - } - - Pen pen = dashed ? Pens.Dash(color, 5f) : Pens.Solid(color, 5f); - - // clipped = new RectangularPolygon(RectangleF.FromLTRB(60, 260, 200, 280)); - - provider.RunValidatingProcessorTest( - x => x.Draw(pen, clipped), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 6041efac3..a42b82531 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -47,6 +47,7 @@ public void DrawBeziers(TestImageProvider provider, string color appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), provider, testDetails, appendPixelTypeToFileName: false, @@ -218,6 +219,65 @@ public void DrawLines_JointStyleMiter(TestImageProvider provider DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, true, false, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, true, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, true)] + public void DrawComplexPolygon(TestImageProvider provider, bool overlap, bool transparent, bool dashed) + where TPixel : unmanaged, IPixel + { + Polygon simplePath = new(new LinearLineSegment( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300))); + + Polygon hole1 = new(new LinearLineSegment( + new Vector2(37, 85), + overlap ? new Vector2(130, 40) : new Vector2(93, 85), + new Vector2(65, 137))); + + IPath clipped = simplePath.Clip(hole1); + + Color color = Color.White; + if (transparent) + { + color = color.WithAlpha(150 / 255F); + } + + string testDetails = string.Empty; + if (overlap) + { + testDetails += "_Overlap"; + } + + if (transparent) + { + testDetails += "_Transparent"; + } + + if (dashed) + { + testDetails += "_Dashed"; + } + + Pen pen = dashed ? Pens.Dash(color, 5F) : Pens.Solid(color, 5F); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(pen, clipped))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + private static void DrawLinesImpl( TestImageProvider provider, string colorName, diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png deleted file mode 100644 index 0e1070e7b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb552c825e6395c17eebfcc42be5b34cb0912bbbb0ada7689fdc80d1f5a22c9a -size 4466 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png deleted file mode 100644 index 0b71e15ae..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1efe570c8fa8654a615adb12fe993316233dc7af7d317a4cc334ac86aa3f5a44 -size 8166 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png deleted file mode 100644 index 0e639df54..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e75557d8e59a3aae917228cfd9b6a1251c5c4f771d08d8e16e2de99cb16d53d4 -size 6169 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png deleted file mode 100644 index 9a7f7901f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57bd54dc3d42753e9d866785d8efa8ec0a79398de26325913973b005d40cd387 -size 4139 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png new file mode 100644 index 000000000..eaff6def2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8cd1828f46fad17c8845c894ee076b6e2c606fae979014d929f97f11643223d +size 6662 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png new file mode 100644 index 000000000..2aadc6ef1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:beb6bd5f88e1dbbfa1a5ef27a3133891250aa7ad0f49522c8e9acea6fbaf339d +size 8936 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png new file mode 100644 index 000000000..dade34494 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f4e27ede09125901954ef4ed489b7e2e44db93c9b15d2cfb4683a85dcf91b58 +size 7416 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png new file mode 100644 index 000000000..84836faab --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e685adf5fdb7809d25f0d9915cffc7d7e583ec890b4381c789062113f3fc54d +size 6431 From 42c80f4b585fd5597dd201cb6d87011f704efa91 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:02:17 +1000 Subject: [PATCH 048/136] Merge DrawPath/DrawPolygon tests into canvas tests --- .../Drawing/DrawPathTests.cs | 126 ---------- .../Drawing/DrawPolygonTests.cs | 64 ----- ...rocessWithDrawingCanvasTests.Primitives.cs | 229 ++++++++++++++++++ .../DrawPathTests/DrawPathClippedOnTop.png | 3 - .../DrawPath_HotPink_A150_T5.png | 3 - .../DrawPath_HotPink_A255_T5.png | 3 - .../DrawPathTests/DrawPath_Red_A255_T3.png | 3 - .../DrawPath_White_A255_T1.5.png | 3 - .../DrawPathTests/DrawPath_White_A255_T15.png | 3 - .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | 3 - .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | 3 - .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | 3 - ...gon_Rgba32_White_A(1)_T(5)_NoAntialias.png | 3 - ...sformed_Rgba32_BasicTestPattern250x350.png | 3 - ...sformed_Rgba32_BasicTestPattern100x100.png | 3 - .../DrawPathCircleUsingAddArc_359.png} | 0 .../DrawPathCircleUsingAddArc_360.png} | 0 .../DrawPathCircleUsingArcTo_False.png} | 0 .../DrawPathCircleUsingArcTo_True.png} | 0 .../DrawPathClippedOnTop.png | 3 + ...ndingOffEdgeOfImageShouldNotBeCropped.png} | 0 .../DrawPath_HotPink_A150_T5.png | 3 + .../DrawPath_HotPink_A255_T5.png | 3 + .../DrawPath_Red_A255_T3.png | 3 + .../DrawPath_White_A255_T1.5.png | 3 + .../DrawPath_White_A255_T15.png | 3 + ...sformed_Rgba32_BasicTestPattern100x100.png | 3 + .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | 3 + .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | 3 + .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | 3 + ...gon_Rgba32_White_A(1)_T(5)_NoAntialias.png | 3 + ...sformed_Rgba32_BasicTestPattern250x350.png | 3 + 32 files changed, 265 insertions(+), 226 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png rename tests/Images/ReferenceOutput/Drawing/{DrawPathTests/DrawCircleUsingAddArc_359.png => ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawPathTests/DrawCircleUsingAddArc_360.png => ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawPathTests/DrawCircleUsingArcTo_False.png => ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{DrawPathTests/DrawCircleUsingArcTo_True.png => ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png rename tests/Images/ReferenceOutput/Drawing/{DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png => ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs deleted file mode 100644 index b01b83c11..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawPathTests -{ - public static readonly TheoryData DrawPathData = - new() - { - { "White", 255, 1.5f }, - { "Red", 255, 3 }, - { "HotPink", 255, 5 }, - { "HotPink", 150, 5 }, - { "White", 255, 15 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(DrawPathData), 300, 600, "Blue", PixelTypes.Rgba32)] - public void DrawPath(TestImageProvider provider, string colorName, byte alpha, float thickness) - where TPixel : unmanaged, IPixel - { - LinearLineSegment linearSegment = new( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300)); - CubicBezierLineSegment bezierSegment = new( - new Vector2(50, 300), - new Vector2(500, 500), - new Vector2(60, 10), - new Vector2(10, 400)); - - ArcLineSegment ellipticArcSegment1 = new(new Vector2(10, 400), new Vector2(150, 450), new SizeF((float)Math.Sqrt(5525), 40), GeometryUtilities.RadianToDegree((float)Math.Atan2(25, 70)), true, true); - ArcLineSegment ellipticArcSegment2 = new(new PointF(150, 450), new PointF(149F, 450), new SizeF(140, 70), 0, true, true); - - Path path = new(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2); - - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); - - FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; - - provider.RunValidatingProcessorTest( - x => x.Draw(color, thickness, path), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(256, 256, "Black", PixelTypes.Rgba32)] - public void PathExtendingOffEdgeOfImageShouldNotBeCropped(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Color color = Color.White; - SolidPen pen = Pens.Solid(color, 5f); - - provider.RunValidatingProcessorTest( - x => - { - for (int i = 0; i < 300; i += 20) - { - PointF[] points = [new Vector2(100, 2), new Vector2(-10, i)]; - x.DrawLine(pen, points); - } - }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(40, 40, "White", PixelTypes.Rgba32)] - public void DrawPathClippedOnTop(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] points = - [ - new(10f, -10f), - new(20f, 20f), - new(30f, -30f) - ]; - - IPath path = new PathBuilder().AddLines(points).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 360)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 359)] - public void DrawCircleUsingAddArc(TestImageProvider provider, float sweep) - where TPixel : unmanaged, IPixel - { - IPath path = new PathBuilder().AddArc(new Point(150, 150), 50, 50, 0, 40, sweep).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - testOutputDetails: $"{sweep}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, true)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, false)] - public void DrawCircleUsingArcTo(TestImageProvider provider, bool sweep) - where TPixel : unmanaged, IPixel - { - Point origin = new(150, 150); - IPath path = new PathBuilder().MoveTo(origin).ArcTo(50, 50, 0, true, sweep, origin).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - testOutputDetails: $"{sweep}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs deleted file mode 100644 index 425bdba81..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawPolygonTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] - public void DrawPolygon(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - - GraphicsOptions options = new() { Antialias = antialias }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawPolygon(color, thickness, simplePath), - outputDetails, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] - public void DrawPolygon_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) - .DrawPolygon(Color.White, 2.5f, simplePath)); - } - - [Theory] - [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void DrawRectangularPolygon_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - RectangularPolygon polygon = new(25, 25, 50, 50); - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) - .Draw(Color.White, 2.5f, polygon)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index a42b82531..2d4df04c5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -22,6 +22,16 @@ public partial class ProcessWithDrawingCanvasTests { "White", 255, 15F }, }; + public static readonly TheoryData DrawPathData = + new() + { + { "White", 255, 1.5F }, + { "Red", 255, 3F }, + { "HotPink", 255, 5F }, + { "HotPink", 150, 5F }, + { "White", 255, 15F }, + }; + [Theory] [WithSolidFilledImages(nameof(DrawBezierData), 300, 450, "Blue", PixelTypes.Rgba32)] public void DrawBeziers(TestImageProvider provider, string colorName, byte alpha, float thickness) @@ -278,6 +288,225 @@ public void DrawComplexPolygon(TestImageProvider provider, bool appendSourceFileOrDescription: false); } + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1F, 10F, true)] + public void DrawPolygon(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300) + ]; + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + IPath polygon = new Polygon(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(color, thickness), polygon))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] + public void DrawPolygon_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300) + ]; + + IPath polygon = new Polygon(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateSkew( + GeometryUtilities.DegreeToRadian(-15), + 0, + new Vector2(200, 200)) + }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.White, 2.5F), polygon))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.001F), provider); + } + + [Theory] + [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void DrawPolygonRectangular_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(25, 25, 50, 50); + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50)) + }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.White, 2.5F), polygon))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.001F), provider); + } + + [Theory] + [WithSolidFilledImages(nameof(DrawPathData), 300, 600, "Blue", PixelTypes.Rgba32)] + public void DrawPath(TestImageProvider provider, string colorName, byte alpha, float thickness) + where TPixel : unmanaged, IPixel + { + LinearLineSegment linearSegment = new( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300)); + CubicBezierLineSegment bezierSegment = new( + new Vector2(50, 300), + new Vector2(500, 500), + new Vector2(60, 10), + new Vector2(10, 400)); + + ArcLineSegment ellipticArcSegment1 = new(new Vector2(10, 400), new Vector2(150, 450), new SizeF((float)Math.Sqrt(5525), 40), GeometryUtilities.RadianToDegree((float)Math.Atan2(25, 70)), true, true); + ArcLineSegment ellipticArcSegment2 = new(new PointF(150, 450), new PointF(149F, 450), new SizeF(140, 70), 0, true, true); + Path path = new(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2); + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); + FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(color, thickness), path))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(256, 256, "Black", PixelTypes.Rgba32)] + public void DrawPathExtendingOffEdgeOfImageShouldNotBeCropped(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + SolidPen pen = Pens.Solid(Color.White, 5F); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => + { + for (int i = 0; i < 300; i += 20) + { + PointF[] points = [new Vector2(100, 2), new Vector2(-10, i)]; + canvas.DrawLine(pen, points); + } + })); + image.DebugSave( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(40, 40, "White", PixelTypes.Rgba32)] + public void DrawPathClippedOnTop(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new(10F, -10F), + new(20F, 20F), + new(30F, -30F) + ]; + + IPath path = new PathBuilder().AddLines(points).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 360F)] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 359F)] + public void DrawPathCircleUsingAddArc(TestImageProvider provider, float sweep) + where TPixel : unmanaged, IPixel + { + IPath path = new PathBuilder().AddArc(new Point(150, 150), 50, 50, 0, 40, sweep).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, true)] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, false)] + public void DrawPathCircleUsingArcTo(TestImageProvider provider, bool sweep) + where TPixel : unmanaged, IPixel + { + Point origin = new(150, 150); + IPath path = new PathBuilder().MoveTo(origin).ArcTo(50, 50, 0, true, sweep, origin).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + private static void DrawLinesImpl( TestImageProvider provider, string colorName, diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png deleted file mode 100644 index 3d94259f7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b74c1eecb18745be829c3effe3f65fd3a965dd624b0098400342360d7d39dfb7 -size 203 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png deleted file mode 100644 index 2bd89ce82..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9c79f14ec9d1e1042a9f0c0e09ed1f355889bdd74461050c3529e7c2ac677f26 -size 7725 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png deleted file mode 100644 index c5206d91b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ffae375183e7df6a7730206ba27dbfe1d94460ee4af4e5774932c72ee88f0bb6 -size 14745 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png deleted file mode 100644 index c667647ef..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87a6e83e4da825413890e9510bf6a3b516f7ca769e9a245583288c76ef6e31a2 -size 14295 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png deleted file mode 100644 index 130ae7039..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0b90ebe9051af282603dd10e07e7743c8ba1ee81c4f56e163c46075a58678bc -size 7159 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png deleted file mode 100644 index ff7d8b685..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e5d00ab59f163347567cdafc7b1c37c66475dcc4e84de5685214464097ee87a -size 7863 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png deleted file mode 100644 index 141ca9492..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:acfc9b104be88bef18386bd3ef9faad56070f1f808aa4ec162dceec01a3c4352 -size 3841 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png deleted file mode 100644 index 2bbf451ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8be397a2c3ea3aeee259dc407633f0bf3f6146acda86a1d7bd8e75f4ffa42b7 -size 3492 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png deleted file mode 100644 index 609fc3579..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:732040a526d7581c2d2842f0f3c35fed6f2266dd7793128d2bb023b8b986f937 -size 3902 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png deleted file mode 100644 index fb1965988..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba9da410ee320f2de0f95a9b37abb1d9306a19e6e6e50ad8ada02766dbcc78bc -size 1264 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png deleted file mode 100644 index 87e3affc6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:89d4652a3e12deffc5eafb55d14111134ec8e3047ff43caf96d2ac6483cc0ca3 -size 8874 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png deleted file mode 100644 index 6f8346d15..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b39e13b16a16caf2bbb8a086fae6eecab8daf01f0b71cec7b6f6939393f554ac -size 601 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png new file mode 100644 index 000000000..1e09fdf4c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1df7aeef7150b0522594a6f2c4d061ed8bc3b328e0ad9059147b6aafe37d8458 +size 387 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png new file mode 100644 index 000000000..17f5c20a8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfb923de979eb0fa90bc91fd53896e3f5868f91ed566fe10213cc77963a936a5 +size 16000 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png new file mode 100644 index 000000000..886c50592 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:986152391ad5a528022b98441694d7412637c55367f86ef720816c6d2f9ad712 +size 16925 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png new file mode 100644 index 000000000..2789f97c1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd479f524fad24de0bf9aeee4025563d4df65e428f34bd7960b28f60ca839f43 +size 16011 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png new file mode 100644 index 000000000..1f446a1a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3f56677500ac0556b5c11591c2f5a56f5f78fd1c2e9c4fb9f1f0962099451d5 +size 14817 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png new file mode 100644 index 000000000..8ad912ce3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa8c0b20f592bfcc49f680bf890a1d007f0ecad26c466129a004b71e515c2827 +size 15689 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png new file mode 100644 index 000000000..1c9bc57a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc6d3d1bae5c465b013ee557bb49049704e8d846aaf75f1b343feaa022075e63 +size 1131 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..09abafc72 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79fa696362eb25aaf21309907b7c98c1c64832faee486259e6d418ffc00d2fa7 +size 6172 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png new file mode 100644 index 000000000..0705678b8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:775bb255b740a368140b86dbd935f564a439f875afb5850e11b95f25283b4fa2 +size 5781 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png new file mode 100644 index 000000000..f2128189a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7328c3c12a80516f4b40c9f102ba173aacff94de56a52b4b0f0eb7e5e3869e36 +size 5781 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png new file mode 100644 index 000000000..5b98c71a1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:959b77169b16defae22856eb71fdeff006a9592102785fdbe3aef65c70682fbb +size 4311 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png new file mode 100644 index 000000000..1f5ff2754 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83a8335815b3c9f436c85581d3291740e32c99ce1a361bcacf487ee669aaef4c +size 10520 From 3b701b6c59991986436cb5bd1a12fb9d7911e152 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:16:06 +1000 Subject: [PATCH 049/136] Move FillComplexPolygon test to canvas-based tests --- .../Drawing/FillComplexPolygonTests.cs | 55 ------------------- ...rocessWithDrawingCanvasTests.Primitives.cs | 53 ++++++++++++++++++ .../FillComplexPolygon_SolidFill.png} | 0 ...FillComplexPolygon_SolidFill__Overlap.png} | 0 ...ComplexPolygon_SolidFill__Transparent.png} | 0 5 files changed, 53 insertions(+), 55 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs rename tests/Images/ReferenceOutput/Drawing/{FillComplexPolygonTests/ComplexPolygon_SolidFill.png => ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png => ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png => ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png} (100%) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs deleted file mode 100644 index 81d77075f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillComplexPolygonTests -{ - [Theory] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, false)] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, true, false)] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, true)] - public void ComplexPolygon_SolidFill(TestImageProvider provider, bool overlap, bool transparent) - where TPixel : unmanaged, IPixel - { - Polygon simplePath = new(new LinearLineSegment( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300))); - - Polygon hole1 = new(new LinearLineSegment( - new Vector2(37, 85), - overlap ? new Vector2(130, 40) : new Vector2(93, 85), - new Vector2(65, 137))); - - IPath clipped = simplePath.Clip(hole1); - - Color color = Color.HotPink; - if (transparent) - { - color = color.WithAlpha(150 / 255F); - } - - string testDetails = string.Empty; - if (overlap) - { - testDetails += "_Overlap"; - } - - if (transparent) - { - testDetails += "_Transparent"; - } - - provider.RunValidatingProcessorTest( - x => x.Fill(color, clipped), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 2d4df04c5..7dc53d5cc 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -288,6 +288,59 @@ public void DrawComplexPolygon(TestImageProvider provider, bool appendSourceFileOrDescription: false); } + [Theory] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, false)] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, true, false)] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, true)] + public void FillComplexPolygon_SolidFill(TestImageProvider provider, bool overlap, bool transparent) + where TPixel : unmanaged, IPixel + { + Polygon simplePath = new(new LinearLineSegment( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300))); + + Polygon hole1 = new(new LinearLineSegment( + new Vector2(37, 85), + overlap ? new Vector2(130, 40) : new Vector2(93, 85), + new Vector2(65, 137))); + + IPath clipped = simplePath.Clip(hole1); + + Color color = Color.HotPink; + if (transparent) + { + color = color.WithAlpha(150 / 255F); + } + + string testDetails = string.Empty; + if (overlap) + { + testDetails += "_Overlap"; + } + + if (transparent) + { + testDetails += "_Transparent"; + } + + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(clipped, Brushes.Solid(color)))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png From faa36232a2df5fc344becafd3f3520d7f29c8896 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:29:31 +1000 Subject: [PATCH 050/136] Move elliptic gradient tests to ProcessWithCanvas --- .../Drawing/FillEllipticGradientBrushTests.cs | 142 ------------------ ...sWithDrawingCanvasTests.GradientBrushes.cs | 138 +++++++++++++++++ ...arallelEllipsesWithDifferentRatio_0.10.png | Bin 903 -> 0 bytes ...arallelEllipsesWithDifferentRatio_0.40.png | Bin 1579 -> 0 bytes ...arallelEllipsesWithDifferentRatio_0.80.png | Bin 2068 -> 0 bytes ...arallelEllipsesWithDifferentRatio_1.00.png | Bin 2140 -> 0 bytes ...arallelEllipsesWithDifferentRatio_1.20.png | Bin 2371 -> 0 bytes ...arallelEllipsesWithDifferentRatio_1.60.png | Bin 2593 -> 0 bytes ...arallelEllipsesWithDifferentRatio_2.00.png | Bin 2767 -> 0 bytes ...lipsesWithDifferentRatio_0.10_AT_00deg.png | Bin 903 -> 0 bytes ...lipsesWithDifferentRatio_0.10_AT_30deg.png | Bin 1359 -> 0 bytes ...lipsesWithDifferentRatio_0.10_AT_45deg.png | Bin 1384 -> 0 bytes ...lipsesWithDifferentRatio_0.10_AT_90deg.png | Bin 696 -> 0 bytes ...lipsesWithDifferentRatio_0.40_AT_00deg.png | Bin 1579 -> 0 bytes ...lipsesWithDifferentRatio_0.40_AT_30deg.png | Bin 1952 -> 0 bytes ...lipsesWithDifferentRatio_0.40_AT_45deg.png | Bin 2010 -> 0 bytes ...lipsesWithDifferentRatio_0.40_AT_90deg.png | Bin 1206 -> 0 bytes ...lipsesWithDifferentRatio_0.80_AT_00deg.png | Bin 2068 -> 0 bytes ...lipsesWithDifferentRatio_0.80_AT_30deg.png | Bin 2338 -> 0 bytes ...lipsesWithDifferentRatio_0.80_AT_45deg.png | Bin 2211 -> 0 bytes ...lipsesWithDifferentRatio_0.80_AT_90deg.png | Bin 1902 -> 0 bytes ...lipsesWithDifferentRatio_1.00_AT_00deg.png | Bin 2140 -> 0 bytes ...lipsesWithDifferentRatio_1.00_AT_30deg.png | Bin 2060 -> 0 bytes ...lipsesWithDifferentRatio_1.00_AT_45deg.png | Bin 2229 -> 0 bytes ...lipsesWithDifferentRatio_1.00_AT_90deg.png | Bin 2140 -> 0 bytes .../WithEqualColorsReturnsUnicolorImage.png | Bin 118 -> 0 bytes ...arallelEllipsesWithDifferentRatio_0.10.png | 3 + ...arallelEllipsesWithDifferentRatio_0.40.png | 3 + ...arallelEllipsesWithDifferentRatio_0.80.png | 3 + ...arallelEllipsesWithDifferentRatio_1.00.png | 3 + ...arallelEllipsesWithDifferentRatio_1.20.png | 3 + ...arallelEllipsesWithDifferentRatio_1.60.png | 3 + ...arallelEllipsesWithDifferentRatio_2.00.png | 3 + ...lipsesWithDifferentRatio_0.10_AT_00deg.png | 3 + ...lipsesWithDifferentRatio_0.10_AT_30deg.png | 3 + ...lipsesWithDifferentRatio_0.10_AT_45deg.png | 3 + ...lipsesWithDifferentRatio_0.10_AT_90deg.png | 3 + ...lipsesWithDifferentRatio_0.40_AT_00deg.png | 3 + ...lipsesWithDifferentRatio_0.40_AT_30deg.png | 3 + ...lipsesWithDifferentRatio_0.40_AT_45deg.png | 3 + ...lipsesWithDifferentRatio_0.40_AT_90deg.png | 3 + ...lipsesWithDifferentRatio_0.80_AT_00deg.png | 3 + ...lipsesWithDifferentRatio_0.80_AT_30deg.png | 3 + ...lipsesWithDifferentRatio_0.80_AT_45deg.png | 3 + ...lipsesWithDifferentRatio_0.80_AT_90deg.png | 3 + ...lipsesWithDifferentRatio_1.00_AT_00deg.png | 3 + ...lipsesWithDifferentRatio_1.00_AT_30deg.png | 3 + ...lipsesWithDifferentRatio_1.00_AT_45deg.png | 3 + ...lipsesWithDifferentRatio_1.00_AT_90deg.png | 3 + ...ushWithEqualColorsReturnsUnicolorImage.png | 3 + 50 files changed, 210 insertions(+), 142 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_2.00.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs deleted file mode 100644 index 0bedd4d3f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillEllipticGradientBrushTests -{ - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Color red = Color.Red; - - using (Image image = provider.GetImage()) - { - EllipticGradientBrush unicolorLinearGradientBrush = - new( - new Point(0, 0), - new Point(10, 0), - 1.0f, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.2)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.6)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 2.0)] - public void AxisParallelEllipsesWithDifferentRatio( - TestImageProvider provider, - float ratio) - where TPixel : unmanaged, IPixel - { - Color yellow = Color.Yellow; - Color red = Color.Red; - Color black = Color.Black; - - provider.VerifyOperation( - TolerantComparer, - image => - { - EllipticGradientBrush unicolorLinearGradientBrush = new( - new Point(image.Width / 2, image.Height / 2), - new Point(image.Width / 2, image.Width * 2 / 3), - ratio, - GradientRepetitionMode.None, - new ColorStop(0, yellow), - new ColorStop(1, red), - new ColorStop(1, black)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - $"{ratio:F2}", - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 0)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 45)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 90)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 30)] - public void RotatedEllipsesWithDifferentRatio( - TestImageProvider provider, - float ratio, - float rotationInDegree) - where TPixel : unmanaged, IPixel - { - FormattableString variant = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; - - provider.VerifyOperation( - TolerantComparer, - image => - { - Color yellow = Color.Yellow; - Color red = Color.Red; - Color black = Color.Black; - - Point center = new(image.Width / 2, image.Height / 2); - - double rotation = Math.PI * rotationInDegree / 180.0; - double cos = Math.Cos(rotation); - double sin = Math.Sin(rotation); - - int offsetY = image.Height / 6; - int axisX = center.X + (int)-(offsetY * sin); - int axisY = center.Y + (int)(offsetY * cos); - - EllipticGradientBrush unicolorLinearGradientBrush = new( - center, - new Point(axisX, axisY), - ratio, - GradientRepetitionMode.None, - new ColorStop(0, yellow), - new ColorStop(1, red), - new ColorStop(1, black)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs new file mode 100644 index 000000000..4588ca54f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + private static readonly ImageComparer EllipticGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color red = Color.Red; + + using Image image = provider.GetImage(); + + EllipticGradientBrush unicolorEllipticGradientBrush = + new( + new Point(0, 0), + new Point(10, 0), + 1.0F, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(unicolorEllipticGradientBrush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.2)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.6)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 2.0)] + public void FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio(TestImageProvider provider, float ratio) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Color yellow = Color.Yellow; + Color red = Color.Red; + Color black = Color.Black; + + EllipticGradientBrush brush = new( + new Point(image.Width / 2, image.Height / 2), + new Point(image.Width / 2, image.Width * 2 / 3), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + FormattableString outputDetails = $"{ratio:F2}"; + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(brush))); + image.DebugSave(provider, outputDetails, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + EllipticGradientTolerantComparer, + provider, + outputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 30)] + public void FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio( + TestImageProvider provider, + float ratio, + float rotationInDegree) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Color yellow = Color.Yellow; + Color red = Color.Red; + Color black = Color.Black; + + Point center = new(image.Width / 2, image.Height / 2); + + double rotation = Math.PI * rotationInDegree / 180.0; + double cos = Math.Cos(rotation); + double sin = Math.Sin(rotation); + + int offsetY = image.Height / 6; + int axisX = center.X + (int)-(offsetY * sin); + int axisY = center.Y + (int)(offsetY * cos); + + EllipticGradientBrush brush = new( + center, + new Point(axisX, axisY), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + FormattableString outputDetails = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(brush))); + image.DebugSave(provider, outputDetails, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + EllipticGradientTolerantComparer, + provider, + outputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png deleted file mode 100644 index fa2315d737c51b7a5c19e5edf409ceab10576287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72Tw7??FZT^vIyZoR#GFi6T##NncxgJiGgzyIdFjD`m^jw=7nXg{E( zc+@iV{JX@PS41NW&Uk4$GrFCXVdP+8VPaxzY;bT;P!JH{;9vn^LP8LgD5@}oVCEsI z!VvmXdpKo(S#)}#u3F!Nu9|*Fg`OW`&D$g9aIiEQeERu#CEKRk<@)Db6hh*Y`KlX# z@Er0`uoT(TBrq?@D@u@qNs!}tZKr0Ys{)6M!k>#9et(t_{(U!j?qWd>&A#K$svc&r z9I|U`pCPo`fyIey5$}_O8#!9`+BLlhR@j>?$YDCc&h;WU(D2VQ_U&|2SfcO@Xy9_@ zo#Gu1Ki=4--HTBcP-@$a(0y(r&{-TU-Sb)_tIX2oNj3)fx--qR5^F4wIkJV5t#QJm z8D`fxS~l(9erC&+>N8JlnFKv<*4KUza8(y*43KS_VRBFbXri9$MNSt58TO)+$-t04 zwDIAg;{qqnvKcMWU;~=|*kPNYY8a5A){!gB!=#vWV~K_t&|O#6d^5jsJbAX(ohi~9 zY~?Pnl@aDZEBW_dnv&MCPxLPhkN7=Ro8-pW%I87?V*Q@?_je d=)?Iv%=4P2D6N~uYz@pw44$rjF6*2UngCN7BV+&o diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png deleted file mode 100644 index b980bbfe9aaa34dad7cbbeb6dc3f95e349854bab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1579 zcmai!e=yW}7{|A@YS&)dS0voVx8+iZgk^2)`cb=*txJWOmc&TP`LTW!8~4SYag=XL zERwQyr$bQ^Iy$NSDnIh0Oe_tyMeJftw_6ss%}sONn7RAo`8@M^&+~bnndf;vugn81 z##${yEffm1)|2URQ0 z$X`QHD9sK}57$Fy2BbrgkpqsX$0O-9=|F^Hk@oInU1iw;Fu$U@(>~r!F-p!Dzql(b zgMay>*(KdP?_N8$Mo0t^4Pdbt3>v)(`jJitK>)yhw*Qm>e< z@%(-rDl*&L+qAmpn{iSg(Fqc@oMm85M9Bp>vC5FCnC5zbUW#qDJAJE@W%@xrO#5i#5&)w8MY3u>R&YP9LJq|S zsswD%OXO^XH>pcY_j)vjOT3q3MBCL*Bb3vxh@+2Rj)xxrNEsbdBgpq1Sv<0?`@)?5 zIco(6j>>c>E~PbJwcYiL;v_D_z^F=pa(_>+ge215n@0(({XW)1!YB7McN&B?)fK zTu9*O`;BP57O7?k)%5WAd;I^&_uwfG zJ;g& z@dram@U0yTef3g{e*S^zEg>&0Sj{VM?dV^)gQ+RlF>G5HSe!<`&0*=uDV(GdtoUfb zB^NVvdHfp63BAP0DYDko03{xP4o%rzxLdHlZPbOWjr0}Is<46q`Y3OgL(g5wGk*|W z?hKDPR{77e@29mplam852X6+4y5{dub&5_I8PlxMh^>yBTR;ZjzbxNleQa>c5ZXG2N;LXQo)u=CAm0BU|eczZzf`Z zhp8dYTpF6M;!GXPjMM{K2#X$!$W;ya}`cn6J#OC%G z2MMm3=gGiEK1A2!7_Ha)kTXGUN{zF8)Vg;(yK7A44!)3^7>samzC3!sjjb^nkmd7U z4{%%}*SXa~$jusS8_`920zyAfJrs6UGNDB7>}So}B%X}9KU}(1{h>(pbe0a4xdp(~ zw6g_owHro)i;N3jSomeM5af%}F!t~3ksI&O6xKB831UOuG%}kf8Y4U2ZKya3&JpC& zI4iV&6TmFOQv?!eYtZ$X>O`9x?sN=V_}$zT;<#=3lhYbV#4=s5$?fHWm36gv zacCFA4V*k&IcI*Q?y^ds8Yp<++>XQRQ|l^)Hqpmc;-7 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png deleted file mode 100644 index 9a3758c7b4421ba9b7d43977a81acf1962e4c86a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2068 zcmb_deK^y58(+0F4mGb&l>9J+ojTqsEA+!`Vk$;nq8Ia4C^K&nos|e@*2ZL{gS@7d zP1Esuo)pX5LSzi*WGA6GF&3kqb)DyWu6mw7o`23C_xHZP*XO=J_w~I#*Z2C|IOc{? zQqWR>Kp;xaSVwnR=6wm6ob0^s%_@>5Xb6Y{A&}ba@7B(2mfhtqVtt|^kgaWBVnfG; zN&*D3h3o7H;^Rl&kKKqJIk5fk84z#@8UGB1^k09N01V$ZLF_%P%3PVVPBHrp-O=}c zufWOo8oU|;bUiVKz&6660PHaQ3IG8iWC(NvY~xquSJ+qOUu|&ceUa1eD!A_-QdDfKtsL0WqJ~js#2gZ<-bFbM7oO$DZIxe8e|Df~ zy29bm5D_^OpDs$@T$L=Z!j0Jptr|*uC26bno5}%Bfmx*x{-d4xFqLy)wZpZ+GxXch zu2efu%9Jm~aF;&#+HlALr24BBV5t@KogJ6)7-z3uEASJ-jH15or@i;MobzU%mx59p z^>U7l7C6*8KdbMuejq%+S^`~BW#UuN#3nU!?8sB5(ZP{=%i5tn+Y%)H@+*-Zues|( z9D-M0gsY+4^A5q)#2~!;v#SD}?Wu`5fdMj5_bMr-KaD_@JXwBpda!lR7FAJ^2zGOO z3a$`j@a=;yONi0xk*`lvauPHi21b>Z3#7+te>5qds+5_#t$8J#36KnY3c; z+fqkwsWeT>!$vL3lrp%9hM8Yp_IAuo4m}yIe!d9i9>m6&V2Ephf_2I@U|?bUGmkE5 z#oHz0m3Ooqsn$38gNMs)+Z)QVpfXbE^JgB}G1nFP4KM@qp@^`9%iB`y$*zK?Bc`i? z<g-l&4M8AnEyqgutE03cTVYbCA}!9pgue?;tCL_i?t~CTue`x ztEJiMn?{xn8+6HZt>85O@LJ&$+^V);2aHkLnb%2E4Z&Uk0vDnRUehv$vv}kC2ydxA zk^7&w?A=Y@F$2xtlNs)Nw!nviL5J^Xso=jT9erZPO*PNygppmJG?Xq_ZQ@#WMFgID z^wUXq6~Y)%C?tmY!wIzDp=Hb5U-`!eCY4_f92x1Jx+w+m=`5^2N{PhUSzw=EB58lB z#}oV9{hp~AI#3(u(k98$#iDvZzbn9gRgul2?d?=9j^h6~sYon&`*B`7NG4F^NcW{| zlTg)cT|0F2nqQU)-G-%3I2;o<=9m%%s z85Wxv_n0Ua#Qm_JHCRTjx#ZYfBq)OsrN{-1-U>@B2ZLqeaH*{X6@J}~C5_y`Tp!BB zRbW3~?}9cl$BB?_^yb|`B~p?Oq8GR@rke;CMaO?PwduGWiS$^cljy zSSX7)Y#ysuuWR>?9Hv*4p*LkW&xH0nMz4LeDhuFEITI^?_CyIos!BS@inmK2zH~ln z(Qejq=WO{rNNxNru{g|=S&~q#=$`o@S`+$9CX!e)@IW`E;}B!gScQ09Du#G|k*4YhTyzT#6kj(x5 hr|s|e#E+l<)M-6pL$yy+ua((4#2M}8$g)38{ukV`jK}~0 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png deleted file mode 100644 index 8b93c88ba93c5f82fb8150fa417061087a56a2ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2140 zcmai$c~sL^7RQ6<2ZTioAX;d^ANT{5oq*9;S$?bukWiIHKokgS6)chjaD;${RlpEJ z0|!w&U{b`g3}|J-T7{CZ7+a(udm({}VNp;vB~0whnNFvkGk@H7-+SMC@B8Py`+3*D zBzbOE)l)?v5ZeiOw_|cI{Afre`B;6Trc~~Lb1)G`AnNkeK7=dE*D9CrK~w}nqy3{P z2x9I{cptpin>awQ9UHdegB7R=)MJ^pwtDAG0_xB^AxFdUd#%wnzv9F7@ zBr!rQ={^G^Tm)R0k2YjWP(R9w&=oHq3F>zYrGqJ_9$h}%DSij2VfOTz5 zClwNqjR*u*hDpFt7IBZDZAjl(B?hAkx0a*xTF|Rnk;~GUI8y?)RhyB-ndv8F7vY%| zZfuwt*H1X@!Om5wU+gI>XuSdHN-l4W-jberA6-7=&T8t4-FVyTPEltZnJ`S1$V9e{ zi-NI*q(Ff0S#W_R=CR=c`VbrLBW;{e((U*RBXouE(65=$&65DX8137Fo>0|=20I!( zlyoKd`p##Yp4&=Epv5>n0pO>jw;;->U22sYgF9uJJja9pe)5A?A=X{qCaI%K76Xk% zkf{K(cOo_=w1V%>ii#JCdu=}?iGr%Az;!!eqjg9Jeq~6XHmZWDK=-(17x`#Q3w5DF zGG~#xQ&G1wep9;B2A-M1%|>+`8+B!cwz!s#4VGDpWxWZfE#aA$3C+;cdk>QgX1PT` z&^sMIcGq_~k}Sm_BI_;~-0*ylq0pFO!S4M@YZWD7Fq6;O+rU;Af_a?ethM{=)ER!% z!8!^hErXPd(OTPkivA0Lt00$ZM_AO1DeI14=My^U%uu0*|ExO4R2VtsCELmI?Gg(l zd|Jtx%OsjuH4W5Jh!0?9#p|eh-31mdtV(*GpxDS%XdKu_hM&0MGOF@%A`u+|&Y#uy zpF4TR+M&VfNb6hX6Mw_RG$q{1*5$}OB;-sao!xB(T6>^j%hq^tjsX=<*z z{`!8;g%=Il%+Otic#eOW%V4*+;{G$NX?2DVZiQ2O2eiC{Kb%XhD(6JZtrx0eOgdfH zlIRjPIq-AjvH27WGaX!DjUMooCifdy&tp1Z`jFTmY4mR|Y1VMPh1${kVY5*I906~W z8M?3cK$B>_$#O9~tL45kYd_;*htd7H9?5UR*zZjOvP5AVplkkI>|~-LQl6^HsWztQ z*qip2AFHH3$>6r18d*Fk&|vr}@&l7NH-@{81jFll1@qp6l(D%h(o`tk>;Z6DRX@_7>O|xUef&9i|5sVi%d?*5_wo| zU>rj@2HuSqFSbz)zCg04FTu3*?2?>+q$cg7H8*8!qE(;JcDL?EMY6&7F2$dpEp5td zhJ-AKMjOY`miXxR-QxX=*Fc~$Rxh=mmmr=u*NL{s7`#DLK78iJlfH?s918PS!_d3n zS9311!e4@evfmmk4gb(S5JdD6U{{iQ>Pl*hz`I<#t8;p0!Ftu&gwyri7o&V|@#McF z`@VrGR6M8qojT)|UF^xIXwPnYXVwKL$!qdqk+0p30g3Qwy(9KL{>q(oV=1@jtoJBZ znBP3G`4TvP*KcU*fC5!Y(u)Q0$L*xF#)N`OJpX_~$l0JB#nG2$=N6j0(w+9+8aEc&lX5~e zmIqr`hr|xKH6|mp$BTMWZcI6u?rR`uOYu#1`XIkd3spNJBmr#wlGCef-Qj0C5=+gp zon2CzNQGh$Q+9+r)o+Yk2_ILa^=VutF&dgtZ7oUwuE~07msJUtmZA)g54%pJf<**M zaM(mI0XvK_m1c=|H47uFGr1EOsUsb()z?ReioU@%5aTYS7J(Rv$=gWg%aKVp~pGK z^X7>dUEE0&Bzy{;4<*JgAHZbw!mN>l4HV*c7ugO|3d}*roOgxKzzsCOKP}jt@EQ1Q zz%Xz3Qgj#i{qfGsWl;n!#GUoti?a5njaJmuc)Ny{*e(Tc`q|Oj9{YtqcoUl;#dR4Z zpgLY_GLkMHEzF0g)dk}=yS%B{a%gz)uYnjPSjMbk*VrU3d|7Zi8fI;|#utNG7BK49 zo=JP_Pf+huaQrm&J^@D*@H=(?KODio()#MwjxB|5Uh3nHt^8N=?*W3~PI9Yp31j>d D^-P}2 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png deleted file mode 100644 index 7318e2365b8507478c81860bbdcef6d8f6aa2976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2371 zcmZ{mdpy(oAIGONnuRtkQRFKl_Z}`QBi78=3Wo_p<1~|`2)V8B>q|BF&2c%L=!dx! z6S?H>lPRpWYDz zg0P6=lx7=5^!LU?bFoISr2~b==Uyg4z@Pj;!E#>#YUktun+n9TdT<(5nS2}gT-fw|d&)>@LJcg_ z>|B44=@2hWS0&jY{p_G`wLwKo&g1pe_Ga1xSp>tCHsy*$%i*(GfIE@>@TdWiU7^Qi zZ7A55{0Vf^vDprZnaU`<8%6>)_!sR)Alk4&Z{sKc2` zVXK}3x$#u@TDq<9ldRAwJCg&&XAos{!PDA7x598F?RMja`AOYMdXBX$l+RdaEIS|A zqsbv)Q=$tANW2AOHl@(|Afp&YQOi#YP1ZDN=BObohfQ5J8Yu4nRf#YCO@H@)AD*R#B9?1wrx>%#}@ z9&J5P?H0rLBpt1J1;?D4S?1mYFR#R;0pm1L%pl#g*u@-)c^*g;lBq-g0lZ&n5mnDY zvlhKJYxM2?Z0pCwDa(4}PdN^+?F%~BgB3eOJE37Sk=6X~=_13`0R5RzW{TtV5BF`2 z%G~hJhDoVwVDZq4QGcWXgk-O9ei0%`#z>Z$^wn7 ziiQo=N2^1Te)jQ>=_+@JH;*<7^a61RZpHg_(fqi^JL-scQ?998Z+%mqx-+fKBuciR zZ%xs3rlIRbi;tRIRZk)?d4j1`Kz+y$k4%D5UT9FHdoasc4+_W^RweO0D6U-k#PzyP z-otTw1g>QQfn@M8!<`|jB$dIetO93)1cOP^SgT|0Ia4^Vq;emcO#3}V35%1(gs`7L zIn9E}mnkFN-N7EEQb_-U?Fp-khJw~*OvD(B!UmkxVeYIeUhYH#$8^m+j1J>b#Z9Eb zeT2m@HsgY0&SC^64#8#1>G&9By4jWt-Q|nxQuQwLN0ts>8owv&{$%IzPVX_g-!6A? zMJ{o{E(*Hmvvo!`d4as4=Qr;H5E#WCtG$@rSE3>Ov>F#IykPxt4fA;DM3WC}DdDV3 z)K+=qrR?BOqv!-Zb3&S~05?TtzB;F(Sc;PQGB8V*>eL_E44;Yg&Wc9xd}Df*6J`t_ zn}*mDI)y`>H;{O^YVSIPbEvs8T zyxG1<9SA~l?eRr%gyttng>%m&Gk}NvPse%R7#)KcQC8P|*@Xl^yoQmJmiU`DN|%o; zuUuCeQCvKjdE3sz=i|zo{V~ROHJ=vdnRO6ux-rkPO^&gK5*udQN?(&`o!i%8lp-K! z;Lvla;yk+paV9>vE!{AS0jqhgC;FOaW<*(N0D}7$J>ypO3lF~W!xghK%_?V!2_Bjx zPpr0yGJ;l$+`E)|qZ;UMy(&$@*0+j7N8KFlyymaAABCF{mEWRo&Bshpke057W24GP z5BMvAX$r)<`tdeF?Q-Lz_1^pMWN^IZXZBl&$smyij{IMp(4aAgGixdB!}w4kbT#)c z4x6a5+rVG-Hj(I3O!QI&Q7?RG(-pv#%fH$v-`{5Sjm1c&a;TPj@hwZ9`E%$)y#Yrn};TzEFI@XS0=$}a}!R$I^uu*GR#xlB0ur5Wm84lHntnq^=?PJ_%| zwwE9#Z=r_eqrvYl@uE_iXy0x7;rJWm4Tse8tyTh)BdZ<~%s3$6b=*OJB$}qYUb>Yp zEUO9>a2@Xz3?EPqH4^Z+T^T3-K=*6S?Ior@JtI3zvSt%g!!uh2R@${W@1l~PV|i4! z?O(8nX?3y(?bfS)GSafVhI9B)>N|^!BlzmvB`P#4-syNt0Mzvu-!~DhsXq0hj>* z`-_0b!5|<6_=)U-ccMQ_?~XypE={}OU7FI`qNVGyGsG|6(cbw5LC8cq%W{j~!2bc? CDd95! diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png deleted file mode 100644 index 41683430e5951e39a0de6cf419b315291bc85d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2593 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 u8UAkk-`)``M_?e&5lKhj! diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png deleted file mode 100644 index fa2315d737c51b7a5c19e5edf409ceab10576287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72Tw7??FZT^vIyZoR#GFi6T##NncxgJiGgzyIdFjD`m^jw=7nXg{E( zc+@iV{JX@PS41NW&Uk4$GrFCXVdP+8VPaxzY;bT;P!JH{;9vn^LP8LgD5@}oVCEsI z!VvmXdpKo(S#)}#u3F!Nu9|*Fg`OW`&D$g9aIiEQeERu#CEKRk<@)Db6hh*Y`KlX# z@Er0`uoT(TBrq?@D@u@qNs!}tZKr0Ys{)6M!k>#9et(t_{(U!j?qWd>&A#K$svc&r z9I|U`pCPo`fyIey5$}_O8#!9`+BLlhR@j>?$YDCc&h;WU(D2VQ_U&|2SfcO@Xy9_@ zo#Gu1Ki=4--HTBcP-@$a(0y(r&{-TU-Sb)_tIX2oNj3)fx--qR5^F4wIkJV5t#QJm z8D`fxS~l(9erC&+>N8JlnFKv<*4KUza8(y*43KS_VRBFbXri9$MNSt58TO)+$-t04 zwDIAg;{qqnvKcMWU;~=|*kPNYY8a5A){!gB!=#vWV~K_t&|O#6d^5jsJbAX(ohi~9 zY~?Pnl@aDZEBW_dnv&MCPxLPhkN7=Ro8-pW%I87?V*Q@?_je d=)?Iv%=4P2D6N~uYz@pw44$rjF6*2UngCN7BV+&o diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png deleted file mode 100644 index cf2981f606fde90311bdfed316625274a4bd9d10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1359 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 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png deleted file mode 100644 index 0f315979d3146eaab36aa35ad9260ca596ca13b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72Tw7?`3wT^vIyZoR#p>wPFd;`qa}SGJtl_y71B9aH&hlJ}ZtH*+sk z=y=_<|9R|@FUPlKUN(tkjxjCebWl(b5a8foVPRroY;16FPyk^~{$(O;`;r=Rm&<178|$A;oJA!R<#6~9T| zUj8=e_NM;RQSmb!>TH`re^zv@P3qq$bKJ-^h}~#4ljI|bYs)-dF`X}*cd0K_p?#vm zwoKIt>-eAAZtL6|do8c#I@3%NN z$X`QHD9sK}57$Fy2BbrgkpqsX$0O-9=|F^Hk@oInU1iw;Fu$U@(>~r!F-p!Dzql(b zgMay>*(KdP?_N8$Mo0t^4Pdbt3>v)(`jJitK>)yhw*Qm>e< z@%(-rDl*&L+qAmpn{iSg(Fqc@oMm85M9Bp>vC5FCnC5zbUW#qDJAJE@W%@xrO#5i#5&)w8MY3u>R&YP9LJq|S zsswD%OXO^XH>pcY_j)vjOT3q3MBCL*Bb3vxh@+2Rj)xxrNEsbdBgpq1Sv<0?`@)?5 zIco(6j>>c>E~PbJwcYiL;v_D_z^F=pa(_>+ge215n@0(({XW)1!YB7McN&B?)fK zTu9*O`;BP57O7?k)%5WAd;I^&_uwfG zJ;g& z@dram@U0yTef3g{e*S^zEg>&0Sj{VM?dV^)gQ+RlF>G5HSe!<`&0*=uDV(GdtoUfb zB^NVvdHfp63BAP0DYDko03{xP4o%rzxLdHlZPbOWjr0}Is<46q`Y3OgL(g5wGk*|W z?hKDPR{77e@29mplam852X6+4y5{dub&5_I8PlxMh^>yBTR;ZjzbxNleQa>c5ZXG2N;LXQo)u=CAm0BU|eczZzf`Z zhp8dYTpF6M;!GXPjMM{K2#X$!$W;ya}`cn6J#OC%G z2MMm3=gGiEK1A2!7_Ha)kTXGUN{zF8)Vg;(yK7A44!)3^7>samzC3!sjjb^nkmd7U z4{%%}*SXa~$jusS8_`920zyAfJrs6UGNDB7>}So}B%X}9KU}(1{h>(pbe0a4xdp(~ zw6g_owHro)i;N3jSomeM5af%}F!t~3ksI&O6xKB831UOuG%}kf8Y4U2ZKya3&JpC& zI4iV&6TmFOQv?!eYtZ$X>O`9x?sN=V_}$zT;<#=3lhYbV#4=s5$?fHWm36gv zacCFA4V*k&IcI*Q?y^ds8Yp<++>XQRQ|l^)Hqpmc;-7 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png deleted file mode 100644 index aa53fe65082ce8b9e7c0957bb2ece2326b66d363..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1952 zcmbVNdo@kqCeQu5G> z*F>GF>`aiND(MP4RRqPBs#${^^~=~$3<-*=jkCLRc2D=5J!k*8-+S)o-1EKn^L^aU ztuM*m#}K9n2n2G>hv*rg@v;MhYirh~pk}_t!LGQI+#!$_md?A#Lz?(V5-~U#0{OV- zz_j`k|44&C4xjh&bpKuY)bh+ys)4t0-iz{*hgxNl&i$omEFtf#<6}$T#qY|6jq_g& zw0;J9-;z!)K8G1zX}S%jgO^pDK{ z$oc^K*M9$&^#SzHs$&K8^7o9Y{TSTQwioC0;XUcx_v=X*tb7Eb2_C69Nb3ASTTtCFt-sQrq##00yVGxQ9hnH;}Q?u z)^F$+c0M}I=^UVd)U2X7a~{gnfr93Z)DlW(g5ADApUYdu3Tx)hdJV1$a${<&h8GM6 z`>q(SFBNrXmi&Q;Ey%=#P9;*>`h2REXUE0qxv+G0gPYn(?#ja9K-I7u9tNi`BWlR z*NUd8zKz@V?B&sSiA6N!Gy=3i3``!kmEarWcm^{Hatyrj^K$|LAEo~JV7%vC=qjbx56 zB>r;yYE}KsPxCR@L_anuJpTcqPhIg>6G0Gw3sqRFWBk#g9(o9gld)?Ge!?}BiPhU@ znCerq@2k+e;WI@PS~kF@^0CEg-wyk!Lu10|LfYPK;-apjn+1D$&0G)y;6gy+%J+&_ zVr%YBNzX-KNl&J36R-yT<<{Ga*LCVY8|U5YbJ1`UFFeJOIonKv@fI@- zLCQhNsbw@d!L5z#%wcm>(1+9l~2^ua91T=jzXi6@&rOw>WRchDmv-$K_kp zu5@auUq)^2CZ2)bA9P&rI7>%#sj#tS781mMd+rKD7#BRX6BBg=M0u}|+jE!QLgDph zya}`<2gS{lP#b)1gN=v>`bVJ3D@Ata(ScO7bI%a|Pe$EcyH1{rmSWoH$p`JoH~qNP z`4NZ8o0p+v)S4>1`Lme4EBQ=xyecQq$1ym!YndwB9NUNMit*Y&)*KTI-< z84XIf8_V5!y63EE{v?UB<_oWXXVU$KXXG4TSo?FyYQc$+gb4>N9Zw)^n=8_t4P@3h zLotze{5KQAyQWP?XD+$}I1I2WDV(`)yC(HxL;XwpeTs9W<&#LtSd zzK+kwCu-AexT9nVA+riqz5A2d=a=sF6E>F{#ivfmzghST!#1u0C$)Y}>4GY4xkKt_ zd(#y_G_NdkUYxmA=8x5FRl2%!=R~lqk zhxQOrRQuyoO=TGwavN2~X7Y^qrEY z4>LCIV*+yGQ@)Z=H$O5iBs{4>33qq(-E7%duh+wAEBrzb?RkUnqy#*T6X8E*rBUM! z1tf5i0!SQ1QhHzpY%B#G5rE!JO=B@1C3KDNxD9-Ha2-YXqXvq|)oCKmPjpS$TDD{377twI=Ym4uA zL5V_4fIas0th8g|w@0Ao28k;Sv?IGXspXV_Sc!FHBQ@X@u_5}EU^kS2x2qd8tkh+7 z)5WH#SzEI=iF&dZ1&qqo+;rDBUA{l`6qa%f z+cvFMGm=dQG^49cGvEq6fHzSX3T-)Cmzh_5{Z}FQ|K#Vth|~x21A~45{aW=v@RDHf b4c-yz!p&KB diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png deleted file mode 100644 index e5c294a42db2a288832a1bbdc0c06473e2d36389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2010 zcmd5-`!}2E77qPPgQcPjM+FI=R!*y(7^kFY8|6e0Ep?4s5mnL9ep*G{nyE2LBa*n3 zGE;&UrGltyIyEZpMbWxcjo^r4xm2reCq3t^Gc)J>1M|b)`+eW%+0Wi5YlP zzIF$24#0J%ak=E~gM(z-e%XGjw9WO=UH*@|L2(pcE!mXEQSe`oM;g{7Pho=t$=>noC|~QVW?>1D|{$nPtx`U0=G+8q0E}Z|Ngv z)pMq+#b%al2_^ToQ*gQrhBZbL>HB;2ZjJAJYMRp%zQT1BSh6Cm>yFaFDjRd&n99Pa zKJ>LXE8Hj-^s&R@i@N@5$=XkvL1%k%#|tO2sOEwAe6aGhKqwE3!~801=$yBj)70KK zX=2G9K#+Q(9=VheR3BVQP1tF*RiU0BHs1@O+ZnV_R}NlC17(O8n>eLh@}ap8{YyH- zy5?AZYn*BJsZpQJ%RRD&f6Le)?I1y13D+~QDGDqcwiWDw%Hk?CNgr!>Ue;SMG0R!H z&j#{1!K&b!+O^FrZbLgQVu8bUS*3=NT1Z20EL2OS+lA=0SGIM@m|mGO>jMKg$}1hYVc&T6{9O zKa=t>zG6@}a1Z1JxZAGWJly^`Galt{UzG<-}u*!M5diWPn~7TV!ne3{Vd!rj~k1`N=B{Y&Lh# zbc9^N`=75Re7W$!S&3vMSC2-RfCK?5Rc7nGsmSeiUlG*(-75G+zHstIOALhE zi=&cKLoFE#>TKw{j+#t5%0ERXx!bo`6KAi;DQG-^&0fpV>OBmZt-pw2=rES{Q#h>X zQDpe%a-mXEF(R?&o+kx&(kY*?s1&^D3E-KPHY+&o@H^YChxQ8lxWmPW)TlmCYoV#j z&vT)pk`|XnRiadHrHN_t>CGj%^u~z;f&hcBiR^j z1nClax2&0cHY*4)vS9g21E*ohg)=B66V2gjb{pT+1=Op+8#!m}O)<`oH@;a6dlh2k zURZnVj)?-xb4I#Zxs`Ve|969S8k0MeJ~{!U)EW&P{Yk^7?hc5Z7B9kfN>tH86|m0! zLs84fzQ|oE^KB1@buSgHB8%hk4?&ay+%VzVeAH>CK0$(=QS(#G{dUf$Jcfv%+daCn zOFNyy`_}uU;5vF8ZET(~Z`Z5Yrzdm*-9zl9LnhwUyl5${-aPV4jGCWz)POD?Zm3SI zH(n{048;}Vye;|k>}BoIUrvgO2nY(4AN>{?9;HVZcKv|0h0^vlAa2$P;zJqteGUm# z^fvfG=O=P0LVSFkkbK0A3=g3;T8k*p| zSWs)R|1A_-b-HuLtc_3GKF%c@dzHWa9qC9mAa1( k+tqcc{By6R3*xQ4eoy#or@9`*N|rN>Xk~9%ZgGMBPep57e*gdg diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png deleted file mode 100644 index 39a82ea75d9e20ea25bae784c7d1a5936e589fd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1206 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 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png deleted file mode 100644 index ec42d87b35001905586caf40832a400943f9508a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2338 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 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png deleted file mode 100644 index bac85109c111e7fd2d284a177a606fbea782d1c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2211 zcmb_eXIPWT77lJ;0|G`sDN?>*DAEKmV0Zxi2tB|e7$iY-F%Z;93tj3)2<7sG5Fv;( zWkE$Du0c9ibiqvo0i|4|=>m}wQ3$M|DmUD=yZ8F@{y1~a%z5WM^J8Y-ljZ5*BqyUL z1A#!~T%7H_#GbcfFe&j^>&q?^J7_TI4niOgb9ZkANQ&pu;m-I-2t>YP$0WF6cLO01 zn1hQwcq(r6!^C^9F=zORfai^n`0dqktsl}FmZeL3l0TfC*Y|71Qaj)A^bh}zc8r8e|G#16I1*b+fMFZ#D9W; zkUtguXYL>M#o=^v0Z)dxY9_JpGQayreA|y6*Svb#99KFR#C)ENFup zRDdYU6-Lm%%%OC6Z-HNs&kE5Zuv0bAe0xt4k~hCCyR1+Lbo2>@LR8>>YND;#?bAyn zx+ep3w9`PY&Wq|MG|xQAnB=Y%xR2U%~;t#gjgyh;EWxw_n8VeW%El~_=! zSsIEyD$|dHqNS*bS?9MjIzF$(9WI9R4+hb95I+EouQFoGRY_IQY9)aSDBu2tK3Pmx z25s$nB9sH^;n#77gR%QSn%%ABg)i>|gX#uiSp1CDEFi^>?(Z2kj)L`jW$>Z#9xzJg z`Epj6%8w~a$$R;KZI-5aO|Fd5?2@#iIp@Ka?=z#jlmv1VBeR_2t?jEUF@(cK7<779 zbVfS|=;xQ7+_U@B>`f{33nrhy&6-NtIEh5?HsP_kp;WFf^AtiOc`U4DMOSv!KR#6F zI83vLKOcxh55 zD)Mc0V01rpAC%X@yZ*dji0VADl|^%`j|ugv$pO4AFOQx(e^-ov%Lg}Py+#EVCFO?@ zbY(NrfO;JRj3Xn?W>|^P4sC7%is$^S_1sJbYKly)ds4RB*nT*Fla$SkW7eZl4h-ze zZzn3;f3q&|r%Y8I@xb1CJTFKX@usBVr**qDyE|{=W2k4`m?j%ii;ByGG~z@3?smfu zrZv&`Ddfh-_9MiiVrghmjLzt^2V*r4se?Vo(A`>$vqf$#J(%~=s91RTs7M*~5>EJG z8IAB{zu2e5i^ukN^&dfY%aw1?LWgX-G_l-;;9(0V3|w4^;mbmy+>ECy?Ri7$t@;T@ za;e1C&G8qvjJh86zEDu2mh>IUMqGKU+Zt6QI}j|$Sz5Q)n5gc>vxz=_;wMcl=~*^T za?Q+3DC;_cA-XGavN%p;#hXVDUn59BsBL)0DLw~#(b=clA_?Q~O@{CZ-KzMr4RZog z&`6xLjJ1P9xC<-)=p8o^-n;Kn1It_ShVhEoW9=YDWYJ^%IeBKXfjya}Y67^H1a0h_ zsPke;fW4%m({VDIkrjh!BaZ!P_1xPpLBk=ls!89E@kVm6WfGVy)t4)cr>h3UVavLj2SKljG^dqH z5u?z;B}9=LHL-|&`<-D+FqyM^hnW&F*FWo1Uo$Re?qI!;-PdJ>vVaiG>F(ap<_nhO z<|{x`@~@3`6hF50UE`1krBNJG=#0GrwN3dDC@&4h7+#`>hv438(2Q%}u@EEkIchY3 zN6MRBC*3*hzbUM!5}&CaN2c%U>fu`BI9OM8IA1$)ag>ekF7+$umU(!}zp$Ne{swiu zxVXAj{QH6?We&7qavw*prcu*aZE@B&QpwYmk#D3EF?IQyoy){sAFYdQ#R)S$kGEp% z4ox*g8o3#Frvl1BdjrR+!srIk_or<5v+9!hR`r!foxHC}L7Ue+-;Z^L;u6&_G-b4X zs=PK>bo!OQpxyn_ZF}jmVm%};8j0>@*3A3Z=yAB{U=d3`z1zV<4bH#B+=`nB9~`gE zKK{BHYin76AWS*Gx(8(aq*V$G7xE`Rc|569!8p>F#_tVk(2|BN08i3K@>cnB?%0gr*DYF!0OA7nGT_ zTexPZ#2bx}vtLufuLvd_`fnxtqcnd-Fag;ALBUhWONSFyM0QUcxF5t_2E@g|!=7#T GJ@xNdma-QB diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png deleted file mode 100644 index 9250a255a2f2b5fa4334dfc9f32aae93922477b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1902 zcmaJ?eK^}$7LJ+d1Wg+a)>rF?V3uJTBU&FVKeQ5CsidaU)?%7y5krY6DT!&St*DXE zGRhdATZ4v)HsYhLeN+j9l(bCIWT#T8(_q^)EAK@8j~WM!1Oq_`gwN4iJFTtR>%?PECPE-P9&Vae z_qm@UAdpWGI6p8rWpwsA`~2vBWAD=@KX9GqH_9Zq>p#;0en>esY%eNeAX%((pnQWU zGjl9>>7MZz3OPv$*YZYu3qU{!3j)=GeT0AlFmJ?`a7%7W`0p~!!GAj5%l!}9@45eW zY-M_n^bd$l{BWYL^icoRRfqIc#Vhcwc)v|bO0M)F^*z-2-itpwJXEf?twp{RFR zE@MKM+dHquGPQ@IEG$Q0nkM{SziO+o6t+UGt zEc)J8otAP_AY%c-PrDvY>FzoO7DZMdls6lK0fDWpjM9gxt{Z!#XXk||5k_t1RXIA5Y9$g@ZRjt9PHB5k zep4V`4b+%lelSpnQrkdVnl+b^Q@Bs_J*Jmq)kH*P@GxVIK%_fzN6Tz9Su&tVE~h^{8}xr=%Vd#D92nIjju{oB9Zd((Fx1EuRmSS z#$R|IK-@*f=J>jJ`2HR0CX_93pX_LHd0sFK^u)i8v8)#xM2y`kW_^R~%nYx4_KJgu zu-}P%5kgFrL)pb(qIOLPP90WR*WJFW+BWSaPtCDEzojns@f|*Y>1XyT~;ascZ z6B~4`508*XmP`suUPZa?1-tmY28z{q_q5`%g0V0P$?-zb;b?ecj3oB7GUiCXQ>=maRt8HL?#_^LU+I=Yp&Lq!itD%{s+He${obu#kX z{Bp|0LC6fix4#atez`%9^F7W|JS*&jHU8k1_2r^VwL3aM6>6m5(#N4+Fm;F|6UE!Y zf&*yf+T#S@z}vyuakRSYWV|#$>|y7_{p=cw`j9_0b4P?lH*Ldd$Boj`FQ(-$S;D{z z+BmVJ(y+0dQKz3f@Tj)Jl+?$+rZ=@4+C#L@DCZvkxH}`4l_3`tf5G87DNQ#2#?*H5 zODlt+v;QbWuVQW%k$d^4eVAR$Z3xsa2Td(|;eVSXMaj^Y(_|8yMdHjqJLsQm(%7vm z8aAR=X{(Ry)~Jlqzs~9(#o#0@D*M@wSrO09kivgGxTFeoAq5xNI=^Jq{5c^`3UY^l z<47^H|3Ho;^lZDkI$b6(4cxzx)j_)wS5#V_wv=}G(h2BvBvLA7J~tX7k6&*8Y9(nb zamtJuSY306KH;Ey40OGDkF&mg0S$=CfMXSHww@*7`ABQOl8Lx0bLQZ%wZe7r?YF@F z{@d%*2lh!1#Wl@6PWp{+yx@|X=u|jrgYgazyZYnN4gml)@xMpB(7F_h&nbW4=jrog zkucCAKOZ$cn3BJobfaExY>4d5B?v@I^ZikHrxepuD?HjB>-*g3xeB-&7^XWYb&!iE zI$TF8krAfi^7KKWvLW4=(ENGJjJV04OO}>e>33;fMvc)kK{6#kZ!y#6fGqS+(jGqC zf_k;aYr%k>_m{Em;Z7?NJ+MxFOT7}XHRgs+`BDkh~5#g^T{h{(mQ+Gb$AZO_7It6sk;`@t5FgNzcS7*441|0@OWu>5H#RFf8#+A8)J(NN QBbX4}A-rF`&)2Mf0qJ5UwEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png deleted file mode 100644 index 8b93c88ba93c5f82fb8150fa417061087a56a2ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2140 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 D^-P}2 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png deleted file mode 100644 index 0b26dc5be20313de8ec77870ff1bc4921798e46b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2060 zcma)7X;hO}8jhSm1ENGC3RK~PMi>?sND)B{eyl+V6(Jx6#E<|&6eKJLCD4gj*$JC0 zB6I|;fe-g}?teeZjobMAT4 zPkOp;Q`1$0!C>3ma89QbS@by&DvGu4G^aw5;Nc)1gu%GETi1xninHoPoPR6~w*Ben z*wP+-j|_u-!Eti}eQ3keqnXs3N3@Rg$2Ocp;$^g=M+OmB&> zvIARYEiD968h74;UB@ z=S*mF#^r5PGK18FR{+6wBd!?o-AM}e}Ypy(cYrY`wfeuePb#5 z`*g8K!mv~CLVlBc3PG*wl##+z1WqW*csMf%WYTVqzWcT`6)2vGZKV#7nT~=KWXjWA zHL3@crYzZQ={o-i7+u8f>oqnIwQ+opq2HDO#oa)?m``P-j)zh<65=xfN3w+tmlYu0A zjZYkiFwHKSBt@{}OD|!1r6rNRqt^~PLXUV>P8C3d4chMqYN+XBB6?6alh7}fi{d_v zuT0SPHx;MQA$X~o$OmdopFj=@3WV9FNvxhcitpZ)p*@;zR1nGVv94bfTPJha*$Q0+ zOW?EhnInDdj=k=8r^Xr#{sL#&n|bF$hQ_Vf#AGp@i4UH1EYYjKuhBdnF-{N~GmzCQ zZ1eQ#EUENNTg@bM)&;t!iX`(ApObC-BBm_gaxNA{epu$-?Ki4f`y24fKyn`yIu<`W z3dO;7!oU2eae~WAV!vBvVR7CD&&t}R6$sEsnU#&dn?(wsVJ}r3nhs7qlj@W6`eS=b z!r^PA+HDF=3cI$2u+xh=VkTjrOT?~LBU5*+X0e^ntwXLo{9esBfJVpg#=BaUlo;#- zxLB9aJ+iOp+4ls|9H%>+4d@*MgElBjp*kmsnYvQP8Dxeit<^OUiysUcv6Pz_c zCsxP$;qhG!ac>?DlS;k>aQA^|ROo^4bu`-F)ny!gCG|K~rsq8^%PsM$xN*CIaQalN%Yom6`mg1TWo> zWAXMHdhv;uRT(Flr0kC%Rd#spGz$)fSv6pjy8~7!DlNqQkLlXMsEd(fHKZfaA9|NO z_}XvwnFd?tYMp2>)gWTUr@M$a2(X$)!9Tw)f!$s=eq++pyxLm+x|j1_{P5gTyYoJLh$8X zeh*sgKh3)Kb@==W(?%0e^)qZ;V>mW|?+W!5zCjW$4B!0oCo$ zqRL_~4ihA+GuD=Q>?HlxA``}1N@&E5P~^Pt4!GR+VdvIOUFQF^z0GKw+TLdFxVi0l au6=Q|9;_bO=Sf%CIn2%3(~0A7mhn5Ba)ibJ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png deleted file mode 100644 index 83299be4f15e65baffa807787475a931aa54d18d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2229 zcma)8do+~m8lPHU8rftsMVid#o*9>eD2n=A=OaWeLxy7PB61kIXox8)#f)(~iX7z< zlcC6jF%CfS)9w-AobKKraXYyWZnc;4stzQ5=4uJx|<{+^r1TpVP# zscb_a5VA1d)_ueCzZg<#W341q-P>@$X~-EuAXqe+&!@I*?4|#X_aP$?a?iflCSJrt z5(0trhHW8_i^J~)sb0SwP)-<2NHD2cyi$7bzWVvfjF~oii7qPo{Jq6IbkSS^H$GsP z5oV1fB94*9Q3zlYaC1UYg+)5lONU3|y?51i6;G-ld-xvEG1HGOx zPRtg4TKwhpZ~0^w`l#LQ2)7iRjDiC6Cis%(HmLa#deJRyg>aq~mgB^-W&}3nh#D$O z175$2h3`?6fF;MkxgD#QGsjJUOKT04RiguI;V}-QvQV>ipR(|i>hrzDgfcLvgkX3R zeSQ7DgF#@uk~3108#QPB`9>hc^**n1%d57Q{iUy`3~|*9%{c0aZ<_z@76tRk5<=Dr zSQv#7UIiQXwwc+d_i_eX4A2#-l+yq^3w_1TJiZe}+Km%yQPCL-yh3mkA03+`EET7L z^(pAX4CDROk>DNd;&gvUmM;Li*RRPn;jQV@kIkdZyS^v2GYO_f+uq-~0N29Es4S*u z*>^$I5l*`QiP3Uy^GtugK2=YbM;sVX#%)}m4c4K~5(deZl@=i#t@HEU(QH?1dN5v0 z?h`%~Z1q`o%h(mt-_DV0Ut8!`Al>YHcVefSW3b-yJ1HPj8sb<3uWVsS%(aiT<)|di zRgYU>4xfcBa%L+pCUwuErd7TtRU`($Oe;N%(>ndZRDXN9mJp*T5QO z#h{$5m8YUb@7v&%@hMw+6~jxQ)1x5}I|P_RlRr9R7pp#4p|WtuMP?LJGuoh7WEmUASYOr; zHW`mO2kg3A-8VU;?atjdv#6bm0YWTr#iCNPvAO+9cXOliR(=fZaQ-P=yS>j!#FM5T zE%BaCPsv=*4kAOj8G&_grj>ikWfqoJd7jGxb-r*HjEj69=l|-2)3g{GU`cYUcDJ4p zcuhxIGB*QdMjj*F{A1;mX_GB*PQD{6G$%6PbXe)WIO0FH17(k@JpLI|99Zcvd4h%? z9L9G~yjL(vQ+ypNSdWgEC%Mzf9p$HD!|(agS!VF`<1SqgpXFI-U2Q)s!_3W!GG%o! zD%9Y*fw8$q5z~)-X)-{Fg3#oJ_Yg*&blU)heKE8`#`cAh@|Q${8=7OC;Z?&l=D766 zUyzqeTs?TvS;fmy-1zj$a*4dB&tshWd_D5@gqAI>LwbF&{bB&Y8>vZJT6+t zs^Hznzop98ISijA(z^6zJC^m4(C`EP%XX;Dc#nNav8C$c^IK!v_tqb;KE9HN(LwgM z79L9s4bEtfL^?NB6bRJLEN41*h%RHfTcIC{eTYYEl8yKm;3Spl=F+;Bk+Zim*8CoR zDnkSXz7^zyr)QO|7HkRc980~P%qB0KSA*Kr`Nu~@JfYZ+&I{uhI7mmip)Td&nZ7eI zMfn_kqXd^XgER8GBq;^k+!So8X8<;V5q~iu+{V{4lq}o>8=5C{IjKEY0fv-X@lKyd z-ns)m$#I_!P-gRtHqpjv55Clr`q)`*KcU;#?4H~W(e3Wz4`Y9~>Qo739{`@1CMO!$ z(RHB(`vxt(se9#s{{Sx;93rKkaS`=Ty{#py^KJCnFPH6rsx;cI-cKf@{fR0!W|d#i()xzMtLW{`hp8jhi~?A! zQ*8p232Z`k2=nYM%#jk^^de2XZ{l^HM>e{Zd`@E(^0lLPhh=28Dp&c*_KXiuHFKg& z`O=al?|R2FS5-@^F>JOITZYmCMc{As%)3)$N`uNr3G!}N!2oZDXC_HWYnOXPaT{E4 zv9Gnp6IhC~Wfy|2AYY8@n6%K%E@)Q}uIqGI_(fxE2M+;DCEdt-$}D0KSy?gkIZp614~8@wr*BF?S$fI~Ut3>yy-f0guhw AZ~y=R diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png deleted file mode 100644 index 8b93c88ba93c5f82fb8150fa417061087a56a2ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2140 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 D^-P}2 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png deleted file mode 100644 index fe59554e5804f20914b681cc6afe1e31417692f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@f)buCjv*Ddl7F5*z~Iut$jv-4Aw;oMgoz>G3X{6>0?S&U8U{~S KKbLh*2~7aQ_8gl4 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png new file mode 100644 index 000000000..5c9d0cae6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8a99b608cade5e4188d38fc59c9e79be8e24c568f15b58ebb0f01b08c5e2d50 +size 903 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png new file mode 100644 index 000000000..c5042fd75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e32ebf0685fcc9d4e6b80e4d7ea474cc6d5a68138842fd35e6eddc7e0cdd0b8a +size 1579 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png new file mode 100644 index 000000000..5cac37f4c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9d64bd82b8210eb9d516fc4123da8bcbe7fc04020f0920f72bda956b7a83b0 +size 2068 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png new file mode 100644 index 000000000..ae5f235f1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 +size 2140 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png new file mode 100644 index 000000000..1ae34ed28 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50a287f41774d0465ea2177e3c87eaa309f8df5b3a58aee6331149e5b26ab989 +size 2371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png new file mode 100644 index 000000000..0c599000b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:951590c849ed03705712af8af70d0f59ffb55a8e48b45c4dc278259ec15f2ca3 +size 2593 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png new file mode 100644 index 000000000..2f38a9502 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21c39fa0aa7a52eda377fc25089fd62470eef10691be5bbaf4ca72cf89f573de +size 2767 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png new file mode 100644 index 000000000..5c9d0cae6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8a99b608cade5e4188d38fc59c9e79be8e24c568f15b58ebb0f01b08c5e2d50 +size 903 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png new file mode 100644 index 000000000..aad3872b8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa202f9f65d214a7dc1dc61f07d08d135d53884c9772542b9babcbfbdfc06ea2 +size 1359 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png new file mode 100644 index 000000000..8a06ad3bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c10895ea30d5e72e99edaedf2044fce105575f80b58b058b5430fc931dccd2e +size 1384 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png new file mode 100644 index 000000000..1abf93c62 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e7e786bde0d54ab5b0192cdcc03e1932c40683ed91fb247690cb88ac812fc09 +size 696 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png new file mode 100644 index 000000000..c5042fd75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e32ebf0685fcc9d4e6b80e4d7ea474cc6d5a68138842fd35e6eddc7e0cdd0b8a +size 1579 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png new file mode 100644 index 000000000..2ca11b59c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87469226e962a3ff3db25caf1dc5707631c01839e9ef984577f2ad081119a1e9 +size 1952 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png new file mode 100644 index 000000000..4470c5792 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7b85450deb90ed831cdcdf34d31313d4cd037c7935acda748a3c2ef454ba5da +size 2010 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png new file mode 100644 index 000000000..09f47d53b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0623f52c89fe2d6b7ee46941999ee42f0c4c04b7a8d122874b1dfc4ca00f474f +size 1206 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png new file mode 100644 index 000000000..5cac37f4c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9d64bd82b8210eb9d516fc4123da8bcbe7fc04020f0920f72bda956b7a83b0 +size 2068 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png new file mode 100644 index 000000000..e0b5752cd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1d29f46f25b6894ea5eeb9776e6ac49f6864b09fc19eb25e6276615aa98cc99 +size 2338 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png new file mode 100644 index 000000000..d3c717eb6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d299c25acdc26626b31630b137b8bf335c609253563ba996921e38acad58c9f4 +size 2211 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png new file mode 100644 index 000000000..71ac13755 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be605c4896f739c0c412ca72e6985a8765dcbdbc1278618ef7ef987bb9d6afbe +size 1902 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png new file mode 100644 index 000000000..ae5f235f1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 +size 2140 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png new file mode 100644 index 000000000..15a946552 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2624abda9a217e5613ef26af7cbf18b8bd2f67c83b406a4cce58c1f7e54f12a +size 2060 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png new file mode 100644 index 000000000..97603b562 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43882d13eb2e3360a9e1ec771c31c10eaba820e09e76e7f073c69552bc09d41c +size 2229 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png new file mode 100644 index 000000000..ae5f235f1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 +size 2140 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..1fd9d9708 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22fcdd48ddadb352d00032f9fc44076e5aad73964ea481860b6d45cfe848836c +size 118 From 130f106099889262c4a674ede67d19f9461dfdfc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:37:03 +1000 Subject: [PATCH 051/136] Migrate FillImageBrush tests to ProcessWithCanvas --- ...essWithDrawingCanvasTests.ImageBrushes.cs} | 111 ++++++++++-------- .../UseBrushOfDifferentPixelType_Bgra32.png | Bin 30112 -> 0 bytes .../UseBrushOfDifferentPixelType_Rgba32.png | Bin 30112 -> 0 bytes ...mageBrushCanDrawLandscapeImage_Rgba32.png} | 0 ...rushCanDrawNegativeOffsetImage_Rgba32.png} | 0 ...llImageBrushCanDrawOffsetImage_Rgba32.png} | 0 ...ImageBrushCanDrawPortraitImage_Rgba32.png} | 0 .../FillImageBrushCanOffsetImage_Rgba32.png} | 0 ...ageBrushCanOffsetViaBrushImage_Rgba32.png} | 0 ...ushUseBrushOfDifferentPixelType_Bgra32.png | 3 + ...ushUseBrushOfDifferentPixelType_Rgba32.png | 3 + 11 files changed, 65 insertions(+), 52 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/FillImageBrushTests.cs => Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs} (60%) delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanDrawOffsetImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanDrawPortraitImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanOffsetImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png => ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs similarity index 60% rename from tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs index 7afd37a42..d6b348d21 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs @@ -1,45 +1,42 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Drawing; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing")] -public class FillImageBrushTests +public partial class ProcessWithDrawingCanvasTests { [Fact] - public void DoesNotDisposeImage() + public void FillImageBrushDoesNotDisposeImage() { - using (Image src = new(5, 5)) + using (Image source = new(5, 5)) { - ImageBrush brush = new(src); - using (Image dest = new(10, 10)) + ImageBrush brush = new(source); + using (Image destination = new(10, 10)) { - dest.Mutate(c => c.Fill(brush, new Rectangle(0, 0, 10, 10))); - dest.Mutate(c => c.Fill(brush, new Rectangle(0, 0, 10, 10))); + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(new Rectangle(0, 0, 10, 10), brush))); + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(new Rectangle(0, 0, 10, 10), brush))); } } } [Theory] [WithTestPatternImage(200, 200, PixelTypes.Rgba32 | PixelTypes.Bgra32)] - public void UseBrushOfDifferentPixelType(TestImageProvider provider) + public void FillImageBrushUseBrushOfDifferentPixelType(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; using Image background = provider.GetImage(); using Image overlay = provider.PixelType == PixelTypes.Rgba32 - ? Image.Load(data) - : Image.Load(data); + ? Image.Load(data) + : Image.Load(data); ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -47,17 +44,17 @@ public void UseBrushOfDifferentPixelType(TestImageProvider provi [Theory] [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] - public void CanDrawLandscapeImage(TestImageProvider provider) + public void FillImageBrushCanDrawLandscapeImage(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; using Image background = provider.GetImage(); using Image overlay = Image.Load(data); - overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 125, 90))); + overlay.Mutate(ctx => ctx.Crop(new Rectangle(0, 0, 125, 90))); ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -65,17 +62,17 @@ public void CanDrawLandscapeImage(TestImageProvider provider) [Theory] [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] - public void CanDrawPortraitImage(TestImageProvider provider) + public void FillImageBrushCanDrawPortraitImage(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; using Image background = provider.GetImage(); using Image overlay = Image.Load(data); - overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 90, 125))); + overlay.Mutate(ctx => ctx.Crop(new Rectangle(0, 0, 90, 125))); ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -83,7 +80,7 @@ public void CanDrawPortraitImage(TestImageProvider provider) [Theory] [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] - public void CanOffsetImage(TestImageProvider provider) + public void FillImageBrushCanOffsetImage(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; @@ -91,8 +88,11 @@ public void CanOffsetImage(TestImageProvider provider) using Image overlay = Image.Load(data); ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(-100, 200, 500, 200))); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new Rectangle(0, 0, 400, 200), brush); + canvas.Fill(new Rectangle(-100, 200, 500, 200), brush); + })); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -100,7 +100,7 @@ public void CanOffsetImage(TestImageProvider provider) [Theory] [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] - public void CanOffsetViaBrushImage(TestImageProvider provider) + public void FillImageBrushCanOffsetViaBrushImage(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; @@ -109,8 +109,12 @@ public void CanOffsetViaBrushImage(TestImageProvider provider) ImageBrush brush = new(overlay); ImageBrush brushOffset = new(overlay, new Point(100, 0)); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); - background.Mutate(c => c.Fill(brushOffset, new RectangularPolygon(0, 200, 400, 200))); + + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new Rectangle(0, 0, 400, 200), brush); + canvas.Fill(new Rectangle(0, 200, 400, 200), brushOffset); + })); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -118,8 +122,8 @@ public void CanOffsetViaBrushImage(TestImageProvider provider) [Theory] [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] - public void CanDrawOffsetImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel + public void FillImageBrushCanDrawOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; using Image background = provider.GetImage(); @@ -127,10 +131,10 @@ public void CanDrawOffsetImage(TestImageProvider provider) using Image templateImage = Image.Load(data); using Image finalTexture = BuildMultiRowTexture(templateImage); - finalTexture.Mutate(c => c.Resize(100, 200)); + finalTexture.Mutate(ctx => ctx.Resize(100, 200)); ImageBrush brush = new(finalTexture); - background.Mutate(c => c.Fill(brush)); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); @@ -139,7 +143,7 @@ Image BuildMultiRowTexture(Image sourceTexture) { int halfWidth = sourceTexture.Width / 2; - Image final = sourceTexture.Clone(x => x.Resize(new ResizeOptions + Image final = sourceTexture.Clone(ctx => ctx.Resize(new ResizeOptions { Size = new Size(templateImage.Width, templateImage.Height * 2), Position = AnchorPositionMode.TopLeft, @@ -153,52 +157,55 @@ Image BuildMultiRowTexture(Image sourceTexture) [Theory] [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] - public void CanDrawNegativeOffsetImage(TestImageProvider provider) + public void FillImageBrushCanDrawNegativeOffsetImage(TestImageProvider provider) where TPixel : unmanaged, IPixel { byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; using Image background = provider.GetImage(); using Image overlay = Image.Load(data); - overlay.Mutate(c => c.Resize(100, 100)); + overlay.Mutate(ctx => ctx.Resize(100, 100)); ImageBrush halfBrush = new(overlay, new RectangleF(50, 0, 50, 100)); ImageBrush fullBrush = new(overlay); - background.Mutate(c => DrawFull(c, new Size(100, 100), fullBrush, halfBrush, background.Width, background.Height)); + + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + FillImageBrushDrawFull(canvas, new Size(100, 100), fullBrush, halfBrush, background.Width, background.Height))); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } - private static void DrawFull(IImageProcessingContext ctx, Size size, ImageBrush brush, ImageBrush halfBrush, int width, int height) + private static void FillImageBrushDrawFull( + IDrawingCanvas canvas, + Size size, + ImageBrush brush, + ImageBrush halfBrush, + int width, + int height) { - int j = 0; - while (j < height) + int y = 0; + while (y < height) { - bool half = false; - int limitWidth = width; - int i = 0; - if ((j / size.Height) % 2 != 0) - { - half = true; - } - - while (i < limitWidth) + bool half = (y / size.Height) % 2 != 0; + int x = 0; + while (x < width) { if (half) { - ctx.Fill(halfBrush, new RectangleF(i, j, size.Width / 2f, size.Height)); - i += (int)(size.Width / 2f); + int halfWidth = size.Width / 2; + canvas.Fill(new Rectangle(x, y, halfWidth, size.Height), halfBrush); + x += halfWidth; half = false; } else { - ctx.Fill(brush, new RectangleF(new PointF(i, j), size)); - i += size.Width; + canvas.Fill(new Rectangle(x, y, size.Width, size.Height), brush); + x += size.Width; } } - j += size.Height; + y += size.Height; } } } diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png deleted file mode 100644 index c0ce4bad148dca5d0c89c4b24e459e7af970a475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30112 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 TVa^{m00000NkvXXu0mjfiMdDf diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png deleted file mode 100644 index c0ce4bad148dca5d0c89c4b24e459e7af970a475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30112 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 TVa^{m00000NkvXXu0mjfiMdDf diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawPortraitImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawPortraitImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png new file mode 100644 index 000000000..7f87f9d47 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0a67021dca36099ae77be86b20481a60d483f565a9dcfa698bdbb9fb3926849 +size 30112 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png new file mode 100644 index 000000000..7f87f9d47 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0a67021dca36099ae77be86b20481a60d483f565a9dcfa698bdbb9fb3926849 +size 30112 From b8c714da44816aa6b532bf60d50f3e9217cde7d1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:47:17 +1000 Subject: [PATCH 052/136] Move linear gradient tests to ProcessWithCanvas --- .../Drawing/FillLinearGradientBrushTests.cs | 463 ------------------ ...sWithDrawingCanvasTests.GradientBrushes.cs | 439 +++++++++++++++++ ...0080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png | Bin 4609 -> 0 bytes ..._[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png | Bin 7641 -> 0 bytes ...EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png | Bin 7701 -> 0 bytes ...EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png | Bin 7634 -> 0 bytes .../BrushApplicatorIsThreadSafeIssue1044.png | Bin 4596 -> 0 bytes ...iagonalReturnsCorrectImages_BottomLeft.png | Bin 1425 -> 0 bytes ...agonalReturnsCorrectImages_BottomRight.png | Bin 1452 -> 0 bytes .../DiagonalReturnsCorrectImages_TopLeft.png | Bin 1468 -> 0 bytes .../DiagonalReturnsCorrectImages_TopRight.png | Bin 1408 -> 0 bytes .../DoesNotDependOnSinglePixelType_Argb32.png | Bin 130 -> 0 bytes .../DoesNotDependOnSinglePixelType_Rgb24.png | Bin 130 -> 0 bytes .../DoesNotDependOnSinglePixelType_Rgba32.png | Bin 130 -> 0 bytes ...HorizontalGradientWithRepMode_DontFill.png | Bin 172 -> 0 bytes .../HorizontalGradientWithRepMode_None.png | Bin 169 -> 0 bytes .../HorizontalGradientWithRepMode_Reflect.png | Bin 189 -> 0 bytes .../HorizontalGradientWithRepMode_Repeat.png | Bin 181 -> 0 bytes .../HorizontalReturnsUnicolorColumns.png | Bin 175 -> 0 bytes ...F0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png | Bin 1350 -> 0 bytes .../VerticalBrushReturnsUnicolorRows.png | Bin 217 -> 0 bytes ...StopsProduceDashedPatterns_0.1_0.3_0.6.png | Bin 329 -> 0 bytes ...sProduceDashedPatterns_0.2_0.4_0.6_0.8.png | Bin 324 -> 0 bytes ...hDoubledStopsProduceDashedPatterns_0.5.png | Bin 319 -> 0 bytes .../WithEqualColorsReturnsUnicolorImage.png | Bin 118 -> 0 bytes ...0080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png | 3 + ..._[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png | 3 + ...EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png | 3 + ...EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png | 3 + ...shBrushApplicatorIsThreadSafeIssue1044.png | 3 + ...iagonalReturnsCorrectImages_BottomLeft.png | 3 + ...agonalReturnsCorrectImages_BottomRight.png | 3 + ...shDiagonalReturnsCorrectImages_TopLeft.png | 3 + ...hDiagonalReturnsCorrectImages_TopRight.png | 3 + ...hDoesNotDependOnSinglePixelType_Argb32.png | 3 + ...shDoesNotDependOnSinglePixelType_Rgb24.png | 3 + ...hDoesNotDependOnSinglePixelType_Rgba32.png | 3 + ...xistingBackground_Rgba32_Blank200x200.png} | 0 ...HorizontalGradientWithRepMode_DontFill.png | 3 + ...rushHorizontalGradientWithRepMode_None.png | 3 + ...hHorizontalGradientWithRepMode_Reflect.png | 3 + ...shHorizontalGradientWithRepMode_Repeat.png | 3 + ...tBrushHorizontalReturnsUnicolorColumns.png | 3 + ...F0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png | 3 + ...illLinearGradientBrushRotatedGradient.png} | 0 ...tBrushVerticalBrushReturnsUnicolorRows.png | 3 + ...StopsProduceDashedPatterns_0.1_0.3_0.6.png | 3 + ...sProduceDashedPatterns_0.2_0.4_0.6_0.8.png | 3 + ...hDoubledStopsProduceDashedPatterns_0.5.png | 3 + ...ushWithEqualColorsReturnsUnicolorImage.png | 3 + 50 files changed, 508 insertions(+), 463 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopLeft.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Argb32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_None.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalReturnsUnicolorColumns.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png rename tests/Images/ReferenceOutput/Drawing/{GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png => ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png rename tests/Images/ReferenceOutput/Drawing/{GradientBrushes/FillLinearGradientBrushTests/RotatedGradient.png => ProcessWithDrawingCanvasTests/FillLinearGradientBrushRotatedGradient.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs deleted file mode 100644 index 9b823400e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Globalization; -using System.Text; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillLinearGradientBrushTests -{ - public static ImageComparer TolerantComparer { get; } = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color red = Color.Red; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(10, 0), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(20, 10, PixelTypes.Rgba32)] - [WithBlankImage(20, 10, PixelTypes.Argb32)] - [WithBlankImage(20, 10, PixelTypes.Rgb24)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - new ColorStop(0, Color.Blue), - new ColorStop(1, Color.Yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - appendSourceFileOrDescription: false); - - [Theory] - [WithBlankImage(500, 10, PixelTypes.Rgba32)] - public void HorizontalReturnsUnicolorColumns(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - false, - false); - - [Theory] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] - public void HorizontalGradientWithRepMode( - TestImageProvider provider, - GradientRepetitionMode repetitionMode) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width / 10, 0), - repetitionMode, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - $"{repetitionMode}", - false, - false); - - [Theory] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] - public void WithDoubledStopsProduceDashedPatterns( - TestImageProvider provider, - float[] pattern) - where TPixel : unmanaged, IPixel - { - string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); - - // ensure the input data is valid - Assert.True(pattern.Length > 0); - - Color black = Color.Black; - Color white = Color.White; - - // create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white. - ColorStop[] colorStops = - Enumerable.Repeat(new ColorStop(0, black), 1) - .Concat( - pattern.SelectMany( - (f, index) => - new[] - { - new ColorStop(f, index % 2 == 0 ? black : white), - new ColorStop(f, index % 2 == 0 ? white : black) - })) - .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? black : white), 1)) - .ToArray(); - - using (Image image = provider.GetImage()) - { - LinearGradientBrush unicolorLinearGradientBrush = - new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave( - provider, - variant, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - // the result must be a black and white pattern, no other color should occur: - Assert.All( - Enumerable.Range(0, image.Width).Select(i => image[i, 0]), - color => Assert.True( - color.Equals(black.ToPixel()) || color.Equals(white.ToPixel()))); - - image.CompareToReferenceOutput( - TolerantComparer, - provider, - variant, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBlankImage(10, 500, PixelTypes.Rgba32)] - public void VerticalBrushReturnsUnicolorRows( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(0, image.Height), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - VerifyAllRowsAreUnicolor(image); - }, - false, - false); - - static void VerifyAllRowsAreUnicolor(Image image) - { - for (int y = 0; y < image.Height; y++) - { - Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); - TPixel firstColorOfRow = row[0]; - foreach (TPixel p in row) - { - Assert.Equal(firstColorOfRow, p); - } - } - } - } - - public enum ImageCorner - { - TopLeft = 0, - TopRight = 1, - BottomLeft = 2, - BottomRight = 3 - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.TopLeft)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.TopRight)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.BottomLeft)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.BottomRight)] - public void DiagonalReturnsCorrectImages( - TestImageProvider provider, - ImageCorner startCorner) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Assert.True(image.Height == image.Width, "For the math check block at the end the image must be squared, but it is not."); - - int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; - int startY = startCorner > ImageCorner.TopRight ? 0 : image.Height - 1; - int endX = image.Height - startX - 1; - int endY = image.Width - startY - 1; - - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = - new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - image.DebugSave( - provider, - startCorner, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - int verticalSign = startY == 0 ? 1 : -1; - int horizontalSign = startX == 0 ? 1 : -1; - - for (int i = 0; i < image.Height; i++) - { - // it's diagonal, so for any (a, a) on the gradient line, for all (a-x, b+x) - +/- depending on the diagonal direction - must be the same color) - TPixel colorOnDiagonal = image[i, i]; - - // TODO: This is incorrect. from -0 to < 0 ?? - int orthoCount = 0; - for (int offset = -orthoCount; offset < orthoCount; offset++) - { - Assert.Equal(colorOnDiagonal, image[i + (horizontalSign * offset), i + (verticalSign * offset)]); - } - } - - image.CompareToReferenceOutput( - TolerantComparer, - provider, - startCorner, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f }, new[] { 0, 1, 2, 0 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f }, new[] { 0, 1, 3 })] - public void ArbitraryGradients( - TestImageProvider provider, - int startX, - int startY, - int endX, - int endY, - float[] stopPositions, - int[] stopColorCodes) - where TPixel : unmanaged, IPixel - { - Color[] colors = - [ - Color.Navy, Color.LightGreen, Color.Yellow, - Color.Red - ]; - - StringBuilder coloringVariant = new(); - ColorStop[] colorStops = new ColorStop[stopPositions.Length]; - - for (int i = 0; i < stopPositions.Length; i++) - { - Color color = colors[stopColorCodes[i % colors.Length]]; - float position = stopPositions[i]; - colorStops[i] = new ColorStop(position, color); - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); - } - - FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; - - provider.VerifyOperation( - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0, 199, 199, new[] { 0f, .25f, .5f, .75f, 1f }, new[] { 0, 1, 2, 3, 4 })] - public void MultiplePointGradients( - TestImageProvider provider, - int startX, - int startY, - int endX, - int endY, - float[] stopPositions, - int[] stopColorCodes) - where TPixel : unmanaged, IPixel - { - Color[] colors = - [ - Color.Black, Color.Blue, Color.Red, - Color.White, Color.Lime - ]; - - StringBuilder coloringVariant = new(); - ColorStop[] colorStops = new ColorStop[stopPositions.Length]; - - for (int i = 0; i < stopPositions.Length; i++) - { - Color color = colors[stopColorCodes[i % colors.Length]]; - float position = stopPositions[i]; - colorStops[i] = new ColorStop(position, color); - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); - } - - FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; - - provider.VerifyOperation( - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void GradientsWithTransparencyOnExistingBackground(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - image => - { - image.Mutate(i => i.Fill(Color.Red)); - image.Mutate(ApplyGloss); - }); - - void ApplyGloss(IImageProcessingContext ctx) - { - Size size = ctx.GetCurrentSize(); - IPathCollection glossPath = BuildGloss(size.Width, size.Height); - GraphicsOptions graphicsOptions = new() - { - Antialias = true, - ColorBlendingMode = PixelColorBlendingMode.Normal, - AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop - }; - LinearGradientBrush linearGradientBrush = new(new Point(0, 0), new Point(0, size.Height / 2), GradientRepetitionMode.Repeat, new ColorStop(0, Color.White.WithAlpha(0.5f)), new ColorStop(1, Color.White.WithAlpha(0.25f))); - ctx.SetGraphicsOptions(graphicsOptions).Fill(linearGradientBrush, glossPath); - } - - IPathCollection BuildGloss(int imageWidth, int imageHeight) - { - PathBuilder pathBuilder = new(); - pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); - pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); - pathBuilder.AddQuadraticBezier(new PointF(imageWidth, imageHeight * 0.4f), new PointF(imageWidth / 2, imageHeight * 0.6f), new PointF(0, imageHeight * 0.4f)); - pathBuilder.CloseFigure(); - return new PathCollection(pathBuilder.Build()); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgb24)] - public void BrushApplicatorIsThreadSafeIssue1044(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - img => - { - PathGradientBrush brush = new( - [new PointF(0, 0), new PointF(200, 0), new PointF(200, 200), new PointF(0, 200), new PointF(0, 0)], - [Color.Red, Color.Yellow, Color.Green, Color.DarkCyan, Color.Red]); - - img.Mutate(m => m.Fill(brush)); - }, - false, - false); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void RotatedGradient(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - // Start -> End along TL->BR, rotated to horizontal via p2 - LinearGradientBrush brush = new( - new Point(0, 0), - new Point(200, 200), - new Point(0, 100), // p2 picks horizontal axis - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - image.Mutate(x => x.Fill(brush)); - }, - false, - false); -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs index 4588ca54f..9d04f5db5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; +using System.Text; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; @@ -11,6 +13,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public partial class ProcessWithDrawingCanvasTests { private static readonly ImageComparer EllipticGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer LinearGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] @@ -135,4 +138,440 @@ public void FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio( appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } + + public enum FillLinearGradientBrushImageCorner + { + TopLeft = 0, + TopRight = 1, + BottomLeft = 2, + BottomRight = 3 + } + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color red = Color.Red; + + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(10, 0), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(20, 10, PixelTypes.Rgba32)] + [WithBlankImage(20, 10, PixelTypes.Argb32)] + [WithBlankImage(20, 10, PixelTypes.Rgb24)] + public void FillLinearGradientBrushDoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, Color.Blue), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + appendSourceFileOrDescription: false); + + [Theory] + [WithBlankImage(500, 10, PixelTypes.Rgba32)] + public void FillLinearGradientBrushHorizontalReturnsUnicolorColumns(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); + + [Theory] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] + public void FillLinearGradientBrushHorizontalGradientWithRepMode( + TestImageProvider provider, + GradientRepetitionMode repetitionMode) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width / 10, 0), + repetitionMode, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"{repetitionMode}", + false, + false); + + [Theory] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] + public void FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns( + TestImageProvider provider, + float[] pattern) + where TPixel : unmanaged, IPixel + { + string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); + + Assert.True(pattern.Length > 0); + + Color black = Color.Black; + Color white = Color.White; + + ColorStop[] colorStops = + Enumerable.Repeat(new ColorStop(0, black), 1) + .Concat( + pattern.SelectMany( + (f, index) => + new[] + { + new ColorStop(f, index % 2 == 0 ? black : white), + new ColorStop(f, index % 2 == 0 ? white : black) + })) + .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? black : white), 1)) + .ToArray(); + + using Image image = provider.GetImage(); + + LinearGradientBrush brush = + new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + image.DebugSave( + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.All( + Enumerable.Range(0, image.Width).Select(i => image[i, 0]), + color => Assert.True( + color.Equals(black.ToPixel()) || color.Equals(white.ToPixel()))); + + image.CompareToReferenceOutput( + LinearGradientTolerantComparer, + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(10, 500, PixelTypes.Rgba32)] + public void FillLinearGradientBrushVerticalBrushReturnsUnicolorRows( + TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(0, image.Height), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + VerifyAllRowsAreUnicolor(image); + }, + false, + false); + + static void VerifyAllRowsAreUnicolor(Image image) + { + for (int y = 0; y < image.Height; y++) + { + Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); + TPixel firstColorOfRow = row[0]; + foreach (TPixel p in row) + { + Assert.Equal(firstColorOfRow, p); + } + } + } + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.TopLeft)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.TopRight)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.BottomLeft)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.BottomRight)] + public void FillLinearGradientBrushDiagonalReturnsCorrectImages( + TestImageProvider provider, + FillLinearGradientBrushImageCorner startCorner) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Assert.True( + image.Height == image.Width, + "For the math check block at the end the image must be squared, but it is not."); + + int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; + int startY = startCorner > FillLinearGradientBrushImageCorner.TopRight ? 0 : image.Height - 1; + int endX = image.Height - startX - 1; + int endY = image.Width - startY - 1; + + LinearGradientBrush brush = + new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave( + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + int verticalSign = startY == 0 ? 1 : -1; + int horizontalSign = startX == 0 ? 1 : -1; + + for (int i = 0; i < image.Height; i++) + { + TPixel colorOnDiagonal = image[i, i]; + int orthoCount = 0; + for (int offset = -orthoCount; offset < orthoCount; offset++) + { + Assert.Equal(colorOnDiagonal, image[i + (horizontalSign * offset), i + (verticalSign * offset)]); + } + } + + image.CompareToReferenceOutput( + LinearGradientTolerantComparer, + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f }, new[] { 0, 1, 2, 0 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f }, new[] { 0, 1, 3 })] + public void FillLinearGradientBrushArbitraryGradients( + TestImageProvider provider, + int startX, + int startY, + int endX, + int endY, + float[] stopPositions, + int[] stopColorCodes) + where TPixel : unmanaged, IPixel + { + Color[] colors = + [ + Color.Navy, Color.LightGreen, Color.Yellow, + Color.Red + ]; + + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; + + for (int i = 0; i < stopPositions.Length; i++) + { + Color color = colors[stopColorCodes[i % colors.Length]]; + float position = stopPositions[i]; + colorStops[i] = new ColorStop(position, color); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); + } + + FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; + + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + variant, + false, + false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0, 199, 199, new[] { 0f, .25f, .5f, .75f, 1f }, new[] { 0, 1, 2, 3, 4 })] + public void FillLinearGradientBrushMultiplePointGradients( + TestImageProvider provider, + int startX, + int startY, + int endX, + int endY, + float[] stopPositions, + int[] stopColorCodes) + where TPixel : unmanaged, IPixel + { + Color[] colors = + [ + Color.Black, Color.Blue, Color.Red, + Color.White, Color.Lime + ]; + + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; + + for (int i = 0; i < stopPositions.Length; i++) + { + Color color = colors[stopColorCodes[i % colors.Length]]; + float position = stopPositions[i]; + colorStops[i] = new ColorStop(position, color); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); + } + + FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; + + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + variant, + false, + false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.VerifyOperation( + image => + { + int width = image.Width; + int height = image.Height; + + image.Mutate(ctx => + { + ctx.Fill(Color.Red); + + DrawingOptions glossOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + ColorBlendingMode = PixelColorBlendingMode.Normal, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop + } + }; + + IPathCollection glossPath = BuildGloss(width, height); + LinearGradientBrush linearGradientBrush = new( + new Point(0, 0), + new Point(0, height / 2), + GradientRepetitionMode.Repeat, + new ColorStop(0, Color.White.WithAlpha(0.5f)), + new ColorStop(1, Color.White.WithAlpha(0.25f))); + + ctx.ProcessWithCanvas(glossOptions, canvas => canvas.Fill(linearGradientBrush, glossPath)); + }); + }); + + static IPathCollection BuildGloss(int imageWidth, int imageHeight) + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); + pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); + pathBuilder.AddQuadraticBezier( + new PointF(imageWidth, imageHeight * 0.4f), + new PointF(imageWidth / 2f, imageHeight * 0.6f), + new PointF(0, imageHeight * 0.4f)); + pathBuilder.CloseFigure(); + return new PathCollection(pathBuilder.Build()); + } + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgb24)] + public void FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + PathGradientBrush brush = new( + [new PointF(0, 0), new PointF(200, 0), new PointF(200, 200), new PointF(0, 200), new PointF(0, 0)], + [Color.Red, Color.Yellow, Color.Green, Color.DarkCyan, Color.Red]); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillLinearGradientBrushRotatedGradient(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(200, 200), + new Point(0, 100), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); } diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png deleted file mode 100644 index 372948bac2459494b6ee366c572dde1b43a925d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4609 zcmeHLZ%kEX7(X|p8{TnU&B4O!-143|Dvo?`6;cX@$=O-fn6qS;;Fis3YyG(-wkXVI zOg3enFNeMa*46Y&OUJ6CB`Hf=-Wj_(Vp=(IyD&i+Tx}u{o%ea)=Xr136V&Fa4}M@g zobx`<`+I)B-}Br%c09dyP4?Wpxr(A>>*Zw?isHG(e(t){d2*`fLbLO0&Zg2QOBLl{ zi*F*F>HMAbTKR^EqWHVmkLTp8`!_1e-Jj@XrR%nwyV85?<@-EJS*dsQ;&}g&@Y^%R%QU-R+eoX4s^J=4~?M1Qz=SF)|`{NQ;1`Qeq%v{e1l`S#HDXW@o zst13a+C0`f(l>r{>g!91*OzwooEaV*4ICYu_~y5JCr3YAIa;+PId-IB=&Qz|1*b1W z`Y+y?eCx+|bDw*(ao)o5eaZT(r=I#^PNcOfka#8bo^~vdC~O*25+zoR-+9=eMB0tQ zrlMGQ#nz&3m7OYF^lf0^o>nnOp@ag%}5gwmW3R;(S*;m zcMAXBvHSniWBGnzu2A#)!i!cP3cL3a#CH1_48;G2kjiIKnc9te#2D_qu}W+JW3f*R zu=ZUp>YG=IDfvfn?^HBU^3Lrje~xBTMYW!fvAyh&OU`b}>7xs?3&iBx^Q+S@jDJrU zM=1G&9K_hXG(Fk1!3XF*+2CbL?)aR*Hd2Fo=spQnCg*K%0egbL`mCSDs;-zYi^ZCC z&A|qB?9Y5cv}a&C(<0um~KiEf)VeARyCnQNR+pOJ0FBOGKB}!+D$M9mcXy+Z$h(1#Ug%A49q_$$hVFx$OTP- z%C`+c6&|pn+_4g>@GC*3-uNKN-qv#wb-}ZFmaiAJR^dII1>@Sm0=|G|FDTXy6(q8z z_r|rBy&(c2v_ZXX2g8NTho(VIJ2;($G`(IBdU=FS0ihEt4xL|UM!{AFSKZ<+(YQ(M zAm>q!gPBwvtn^oBz(y)}5tRc>Ax@(mtbK_gGP!~1C=9*E84Q^n2N_yDt~jgI0l>al zXnG}EtP;)-%(0YzlNsFNc9mBF|8x~NOCo2!qH;(C@JYzQQdDjl5wH{tz7jP39Qz!l za(u0+dItLfWJcMyAE)yd|J3=dg5%iR4g%%6$(*PHiw5v*1=+?40R_$>cBKjOs-Ug~ z;%a*QbV`lO?JQx%fJ0|(7?PPgbTEf&vLiYHV4of)$Up!by$QewRAOQyO19fI@F5*a zMah<$gX4Lqv7E&R=CNe)e3sgd$*DF|7MozYB0i?7!$2yFY|E>lC=rzUdKQv1+6^<> cFr_r_&M*D)+NBofe>jT1dTrTft7^9W0io^zU;qFB diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png deleted file mode 100644 index 87ee84ba5b042274b7e06e029108e4d555f7f449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7641 zcmbtZeNYqW9gf%2t6w;y9#lnWr;gSuA6Y-JQ6TkB$D=b!TY6=v10kSgsA4L5a>A0# zm0GG4x!yUKo#G|qv?FbuGvjB}i^%~(dS=IT%G9ViNh_13n)6cKLvCSBx?0UX@9Rt~ z(?1vn_TznjzxVk)&-453{_k(vFmu}cX&Q}YX3^{GeyGt*`r^^gSEnfd@m&oaP=38w zvv%WJjpjthblEsr`FrZmUoYFG(WLY~`kCZ4{c(#%v#_aX-P#|$b>r^Mf9?qWICb9- zX8)tb-`Vu_%SU#G6TdTHgz zhr5o)dvulueW(wL?9dAt8#W@*6Vd4Aux2O4Bclwj-VxHjh)_Y!ka&vFG zP|kPFSeSTvRGYbMH>dpj@#_XJ$$$ClqnAGZ$KEut;5GHZI^N{j{&dw|SG#oY(3;0P zcyx2L%6)k2ztpSyq_N)}dHUKEF++ImS5IH7<2yNh-GP?>ZeO|Q4Sp1Mq{NdOyjuD6 z?!8mQhtt&kIvG1&OcDL?sFkst{!k4)ONiWW^r_DdYcoyf)jcf|eB!nFi3m(pZUgK% zPbkq%Ph5-|3lE+8IFZ!Fg12g(nzcT81>`B4w+3KSqgsP~Ii{G+sd+mQyN-%IGp|n# zV6iY-pPE?4)=w=%o@4w%oeF|@BX13t2i53`E6q30PI+>$I(|84cS5G-3cKK5zp?PA z>QUzJm%a(nmGRMes}p%Cu8!@743{#7&z+yD0GcO?CswKf^spcF!^x(I7=iL^MX9GJ zkz*P%nXs&awi*|BH)RQ&Z24e2P`xctJoznB{c1R@63&lT zTcuhep5p&K9A~w3U=Mp45H(Bu2GGbxz5_V2feB&h=Lr!J3-6Nr zEuhTuhFZp_InZfhCYCZryt!tfH@z%S5V66C;?IZux;Z zPo<82HUo03qk^|zE>nwTmKZ*NcygjVRxyTnJ}mnYr$?PVAk_dkeH1v|Hmut0xRdM#6?=X19>TVb-Fp|PIzt#j(yTK7Vnv_499VKI z-wL2z#pn^Vee4Yo$|lZZ0*aZ+J5W`p(CEjDDan}`DZQWFyM{a&vU;6?5SGeb$dTfY zaQByF23~ZE0-AY^S2e3a$^+PK^=emAz1U{4%J@l zm&TUB)pCmbJr7T+jYgh8Ig<%E%-fJMMe<4Dhfe}c)o48n(WVz-9$KYk;G~qMqY?jX zldu!5<-MC#Bcj>wdX@{C$y8oSkXN;W& zqAz7aM1{!_5Ff=@o3W+M5qx3Dx2;=d0hGnYLKEOw$9n;HOBhSCj9SAtA&E-6SODd@ zoO5(R78M7NI=qXsL%*o#C8>F2=TAx=fzUx5r&j@Q$FOswc$%T}doLSAaLp){X5cFq%UV6BX4 z4-zhuOqx3%?6%@%FM`{0{YH8RN@=mM0fq=4QC{5b4uNN$PFFx5z_g-A-U0w}7-J{F z>7+4w6keJz>3$b%P<|U#`cb4gx^JWUUDB#JdZG4uoQeD> zL-oTV9DuEd#lb86O@>Y?;WXG+U^J<;V5q5==-FuoGU5TL9(iD%IHChPK@Ui&8Q~FP zM5xCB;)Et$T~K98bF?0ONUqWRHz3PxbQcc(+xCoW20l?n_;_o-yhlzmpIh|Z!$a?m SDc@!_Me8@MJN|0rFa85QSGY$2 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png deleted file mode 100644 index 42bc4d9fdfdfeebafbcdd395377b9b335209e4bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7701 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 JDDwT?{|AtjA9Vl# diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png deleted file mode 100644 index fc5c6ed98f7dec3671a284668823866542691fdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7634 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 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png deleted file mode 100644 index 0f6b4e174b76d67932bf70bcc19fb8cc7c92035b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4596 zcmX9?c{~&T|DU@ygvKJ}s%!~qljPb+b2lR`p(1OG+_^Pmk~2g$*T&{Pi(EO9tJ)kB zDz}P|$;Vkr-}U?b@qWJ_@7Lq?c)nh*=i~KwzF&{`Ba97N3@8T#003g$-X*sH0Le`A3n=^GPbwLs8qH(&&r4?rP5N>Z*~#Z?y>apDC}g zssHrWez~{rne0v9Cf&sT`q97j@Je%_X?XA-tJeFZwl;wuVeQ1Y18a{+;R_BaN4i>m zX+-OYtxbZb1;pE7G7xC+u*5WMZD`>o=^Mq>;MLkM`H<6dEzmaA^Ux|(+Kgh@x1LGK zHDWiKk{>dnIN)6e5b~Cr(^F^ur_mJl`!J~*g+;!~j@?O4Oi7E3i~8@{gYdsb0^pa# zxYd{)Ie=seX;cwLYrIIK?U;nObfNDe%^5gt+6*^tCorK}JvSb-u?;S7Oc)%vl)`ZO zm(ANJWlFggNjePNe%g$+NqEucLVB~xZ?k;HSfJ!3o<}}=N0?7CrJ)$tx+oKG*QKLp zXfY;f0+ab_!^sYmB#lmrtgYl?wLoi}ol9aIV6yb0jEwYL_7fXs&ouMIgfrvmWM_eK zIHxp23R93Cpi`3&%*)qXT<*ko`D$=NnqKViv?(S>xoyg$v^M#%Rc%|m>S7fMV$LTN zgznOT=PsSWH(8l%1#K=Mj z6lWhdKscYFFY?oZiJO2+DDWNEQQ(yUl0E4Sfu4amXkuzgB=XmCKkK?Q{JFWROimB6 z%ru7PE%d0`YFb&>I(m{~15<@EjT^(ptxJ!|Joiy@v8m0|?h}#7`Jr)!0E;ng4+_s5 zE-iR}0gLbCi$7RlS@Ufr;xP&tkhnd*7q_BZ13qtL_(@!1Etop5&i`uvs z&*K#mnn?|hq>pdlTK@&`&cdeVbPSFICU(q!m4B1@WXj0*?JdZMOc#-ZiQ%lR;%oS4 z9oT&dE;@#<7gHy6<6>F2f+11N$16KuCO|8e*0Fa^u=M_hE&EJ zsM-Kkn7`>8WH(@G#iu@Q$m$-7NmeCatuXDLkw}F{REiCl#&uzW!WD@0e4MP_J*nEUkCVB~Cj*)L%>}(+OI=4_BUK8`ZkC zvQt02DS7s~zRQNPe67b)54}|Xe%Tg!FRe;@n+ua(BEw)`qmPm^>s$_>aV!B9@!K?@ z=^r!-&TWfl8lP*t79{?Rfu?I$j(vafHpmlF&ZbpArCOy%AIjHjNT8_=Z%TplR#J*s zx9UgVb&4DbzcMB2)2`jutL)dG6Oz-cHF?KU>HGnSY=QoZDaIa-)zXPFOG4?9aMxY8 zTinZwqc@!?HgXLkg@e>lkBb)M&{r?YA6bv*ll=!Ufa%bNDGA)gFz`to;8#|JGC9Hc zWuEKG&0Tnim$aplv2%^mjORAAu4r>^R6d|*a-(NW+W^Q!o*U0f&h}D@(7kZS)e-1w zwmc%ZuX&vtc4%g+;m=S`*!wJT;@hprcZfmXK&GNYor{J5n4yX0tMW_Z#y}`i`?MFl z_$;!pv+}bSgMw6F_K6RLw$3A|eoMh7DV^4yB*a4hUfxqkpYy$5(@3{GhTg36ExXDq z;eEbk<+I(9ZW~anvDqQ#2Ez}JPD!yhOd(x^{%4irU z=Rw{j>s?i0FUJI>b~{!dJ9km`q&Q*jNtt3kTB%LY=dS9a15**}Sl!T-e$CS#dC+MA zlqd__c8_cs+fsFTa#C~zKnSl&yU=?2pwC%PlEYPC>1lg0n$pJk`)$$pW%W`eUY!6NbFe)8^#j^^Pl)q1ny z;bv*%=v-61PyWmFHw@fctb}dlm4RI6oYBqj9#)ywe z;6UF=e~A&jRxOJ8TOtEUmHfw0|J?PrIpVA2N+uPizuN|hy_yZutL+wsoPG5V&fmi>_}eV(14XAl0X8&`(2b%zl7 z;=v}}sSLGfd=@?yLFQ?lyTU702>Z0jh}(b)8)HqQ90zTKBAL^L7>ki{2D09O9jrtV(&;Giimjd+*hz#IA7(q4+SUZL*Iwr zA3Z}|uXo`8bsd|Chl%ti^?VHj0~c$5x7)1zy}vQk>+}}+>2E;fQ~2X~4UQ?|ISKJn zd?BoD)Osx(ar{8d;Wh)*3&<8sb418H>~f8LTvJCxeD-2iuO7$%2uIR;7wlQXyC@S~ zO(#jmF+;W$bBrO~B@pq}@*WC^L}22EXOfL=%F%R2i2TFRufem=WXlqW^PoJ*+M&YJ zAh48c$q8ArX*0NjOUk!RD0orjTqnl&*bG*5#1yeUOK!>$rCj@RcyE z%i#v%ZsF*h=ZmNfo)4W11-f?Ijw{IBh>2XJQK*%q0m zXqbqfs^|7uSXcB}6=~V0i!quI&-YcfMB)ugd>Y($Rot;1LP9COU?G$GC>GgmHYR_fir(u|kI^VpI2szFvi zLy(y|f6sVk@UfGnmOYK~beJVYZaKZK{buX-jZ}@h|5uR^;(2YSbMKbwSX3bOOXwLM z>^lxC78rCfrPGF15Iptiqexi_Gl4bj7p%q+pIu{b@{dxLPici2ku- z2lFILx1pX~mA+fhx*SAN-8ljFOCHdV#Fj*yt)0Q0=5C6%z#@n+zw_sc#>C+qX*J~4 zS%y~lKac~#v(b1j%K_bi26(S~S3KUD4++1HhJ#k2ZZC}HoPyO<5JO>sA_9veDF7Uu zC(pYbaAot_xuEs0z1heEK0Cd0f4F5U&cg+Yf0rz{7sRr=kBw35G#_PVIMXUkLoHz4={`5FWQ%iTxH>h0P27|4GWywk?1@Woc)qVUPhlTL z9*)18{tscyAgx0%i9zM5`;{DNh5YhzZJlLMV`UWL0bP#27Ho376d#z2O4-T>b4Z6k*sW`SCLQ1F(D2cOsFzZ9ZxmN`@R|8t>HR0mnMvwJQN z2w}7F{EHyC(WQbL+z}MHE9uTBcI{NYvKF{Z@TM~X$KRcC1nU?$J>82<~l|`)U cQ-6{~f1c`C6fHl(`?dj?o7!BcGQJW0KhA!|RR910 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png deleted file mode 100644 index 9d87a3e8b8a6edf6fb9ff6f2d8d4551f3209e067..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1425 zcmY*ZeN0 z`*3EuC?Fz$APCXHjMV4h{2qON0{Fd30=j=?#*5ViAs$5^ z&!+qGWrEm}a4=Q*LaqJQ!jxtuE;w~ce1qeT*YNL|+u!L~Is4Uq{)3)E$K_7VP33_f z*V12~nO$=(75sgdn_joX|M|Y2UdqosB(7(j>+wm9yVYi0p>6W0u=ko|Tx}jWtnpD{ zj-hTcf8*U)DN|-r_cZgVUD|D8)_eiWDOesiXOA>mdiwl?_4cueXGqueBl0=TLy#e1 zGr}NPRR8%_@Lk!2$rA#C>QWj6)|`k;C0&WLp^l;F<~u`Mc#Ns6rIPN@_F0Pw?|C}{ z8L$_}6I3-WHEkHEd!T_U#EQX&Ada-?J&zOAc5M++Z!eiqhBp9}I6|)NjS1zu5~o82 zbjK}Ci8EbNe zE9Utdql9(X)b;z$!1fDCRJcOoJvysIhe+=UXkVT?D-UcJafg7eJ;HOEXLiWs62ast zOGPqic8z7+ zsBsm?bp1M9Wp=ckOh=U5y(q0~3YiKRBd!Xq9+n!QZ^sZ~*08L;Diin#y*uz2ftrGJ zO_;O)3xamAixC&iRx+tT?b@(YO&ui@&A3-R*44 zLGQ(|@e;$xrxFL#GE*-eD174|j3>ST diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png deleted file mode 100644 index c990c85f4933b4a637fbf552381f88a93e0ac362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1452 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 WI(NKXtYCxdmn2BbN*&~#k^c?Bn8{)Q diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png deleted file mode 100644 index 88fbc237750c0af59e194cd1d3b176704c7ad02f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1408 zcmY*ZZA?>V6uwPo3#=bFC#klqon&7baWE&eA*|Gag&3GyOkhDNA1+edT}wK`R+L-h zqn${*AN#S0PTYRzMg%c{GZPC`SAPrwQ_5@+T3O0m=O}BK7gBn6&Shx!~4)LL;tDm)U z8BRkkGWE6tNGHBF_PUG~`%<8qT9D;Jbr7qarkl@+qRs!@? zY+p5=G5QTPsK^=1Kq*4AvPc;=+)ctVlW2#G=Cf-ur$P!1OHqFcDHQ8nY%gs<{&Qwk z%U+SuhF_3@8rq4Z$u#z%EQ7cvXHAJ4PN;e`_zr7OmyF9c{Sk?F*nuGc61b)n_$K0h|D0{>UAblw%h3V2E zmX!Hlj%-BN;>jcgtnY>gW8e7Zb`H&FsYti&77^tRy+sj5oN*$^#)L(I^pZ9%$vz`jEXuFswdMDzTg!>`qAn$m5p< z&{%j}S!9~<%%s4;x9UqH)a35p1IjUt@26}izW(q`a~i>v8!*17s!S&s#d@5MaSj_l zJcZo9c0#ei6GG&7F1h`Vtp9$$7W!p|>MDrF*}F|*w_fCZ;WC?p9pU-qe-`5WeK35w z>S89M4kq_Wx>RiqTX_D3F%zbGr0DK)!~d@qQ-7rvU2(Zi=eQJOrs<@(%AAtIuhupv1n0qBOKi$~>jRU#!($Y{7Bf(aQH6bFXke*mkh zaOxdtIm$4~9c^-oj^Pnhm$SM0am|w&xV(B1D-J4)wW(w0Rc$O8DBWj@anMprDGUi(^Q|t>nby-#_P1|8C68^Z7$f;)0g*ha6H3vL38xY(D6~ YkY_2TkQZ9B52&8O)78&qol`;+04o|M{{R30 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png deleted file mode 100644 index e52fe3a25c5a4b97315df1c613a32f4a9f80118a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+QY`6?zK#qG8~eHcB(eheoCO|{ z#S9F5he4R}c>anMprDGUi(^Q|t>nby-#_P1|8C68^Z7$f;)0g*ha6H3vL38xY(D6~ YkY_2TkQZ9B52&8O)78&qol`;+04o|M{{R30 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png deleted file mode 100644 index e52fe3a25c5a4b97315df1c613a32f4a9f80118a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+QY`6?zK#qG8~eHcB(eheoCO|{ z#S9F5he4R}c>anMprDGUi(^Q|t>nby-#_P1|8C68^Z7$f;)0g*ha6H3vL38xY(D6~ YkY_2TkQZ9B52&8O)78&qol`;+04o|M{{R30 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png deleted file mode 100644 index 993d56068e33127295e3ec7db8ada4493c1a7d03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7Pb8)Z%NhixS;XsNd-O<;Pfnj4m_n$;oAfL0q zBeIx*f$uN~Gak=hkpdL-^K@|xskrs_>PB7$MIM%e&hMB1?_0=nVnTvjQQ5@FYd@ww z@t=0x+>E_^?Ik9Lel2|l21j-V7Fh;?5(bA0j6j6o?>Nn(@{w87Wku~vpverLu6{1- HoD!MlrJlUfW|U-y85}Sb4q9e E0M=(MivR!s diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png deleted file mode 100644 index fb9b583781312001657bec6b4f9adc4a4f0c5698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7Pb8)Z%NhixS;XsNd-O<;Pfnj4m_n$;oAfL0q zBeIx*f$uN~Gak=hkpdKq_jGX#skrs_sv%#4f`IG6`5pKE?+HkF^I}P{Z<^4qwLgw# ze?D<$=AM1okDs01ef&nn-XF1D|2NydN#w7-vG_?WH^YbOCO!rr;;>>+c)`%HfEkFO c{0+MpHQ#*KVt3(b16sx4>FVdQ&MBb@04!cY>;M1& diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png deleted file mode 100644 index 22bdf1d3c2b5027e07be916de368182e8d881c7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7Pb8)Z%NhixS;XsNd-O<;Pfnj4m_n$;oAfL0q zBeIx*f$uN~Gak=hkpdJ9_jGX#skrs_sv*|_1re8ntrC0xcW&K!Sxq95?M>aou=QWm zZSyzPlse3ycho>mdKI;Vst0JsV=0RR91 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png deleted file mode 100644 index fea93e74df202cd7a125e6aa2e862114bde34e3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1350 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@YF>D{IEGZ*dVBj~-Yo|Khl{*F{{OdF$j2Y5RHm`nG~xZ6uvNDY z?KwMfaqQitTR$(|8XP_U?YEo1_n#?0lm5(pU47lU|G#$G)&H}t>Z?opcmICo^Wtb7 zb(v?sEpEO1!h59XP~yIJ<%;HrJc|b$_Y|f{^I0^RcRX!l?t8$SwDVyDRO*G@d-=~% zc@|lZcNCw=28utn*W-HFkX$FzEZ$mhAdva^0arztf-46n?$Kyr?u)Vz0t@pWhe!(_ zX9n}d+vQmJERKP+GV|GT$P_%m%1qg}n7t9Afal?b<^zlnZ3QnJA2c}gGqKC0gY@R~ zg4tC977svbjz6>bqEK*PAxtMo@#Co$r@mnb>ROz_P?;=lvFt6(c({>zg+7;I*2A>) zIX|$-0XYJu3hX$(&c_BIN5P#4lAXbHJg*w+Jg_Q|8HTETcVT9M!vLgZF-za}ujme4 ztkO3fLy);I9m7ZHVl(b9K@Nq(f7D=3J6y94CNtwa-0aUgLD6!C9TZ22b~}JE$oKgm zC_ZQKgW~6KO+g?BJ21)I-?_#@>@;WlPrJVJU%w|=J~X(0{NvVtagg-1E??j@FGw!l zJ}DBIoPP8^e%~JfN>O=*cWOZ?Y}I3q`#6&rYU=w}{9}eVpM}-~39DzV%zauGmyYiN zrZZ0WS0Le4;8by38RTHb;|C1&WeP%$fnvUIVse}XG?DDY%3OIK7WjRJs8MKk93|}} z7o$ppw0-oFQLPpLh5#tM;O1<9sBy75!VfM0j@378$EVnKJZ(Wuqj`lsm`Y{Xjw@oC zdiarr3qLT7;prb_pCNBw>^FsJ!tjIxHvaOX8?G2i1KEy8S7EqylSSMm47VQmct#Fb z62QGbBmO}HbDuhjMW93k^Ukwk@m8oCW}J@|iGp}N(XKo&1M1$NcU%`fZ2=~nGugmY zA}{k9sOAj&@jXCYpSRDijfvU~3@@0#-}C$G|1!4n-cj}aWWF6(5HfhW`njxgN@xNA D0(vF` diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png deleted file mode 100644 index 843833d044c69e6401f1b06a4057a84d4918bc46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^KsMtS4mKc*&U>cv7h@-A}f&3S>O>_ z%)r2R7=#&*=dVZs3RZi%IEGZ*dV6gnFM|RH%K`Jm|5LB>F4$mb>EyX?!O!(`@4Q{U zY0}NaW$i9XAEhr=cep4`^bmOiB2|P!S>G!Pb-E}8ZV(2N6Fovi!O|*RWnk&fRZJjN toh}FcK%`P&r~-(ZsBxpy_&IM@awx diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png deleted file mode 100644 index 91a2e5ba230322e8c38c2b8b2cde73db3fd61906..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGGgAGU?ZmZ`8QY`6?zK#qG8~eHcB(eheoCO|{ z#S9F5he4R}c>anMpx|py7srr_TW>EP)+&i~_O$5|8v1UNWYSeTd?8yg%P6chwNn2-=eC5kExAqJ>j1~TXXbIb?K1=dYi%1)z4*}Q$iB}sTf94 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png deleted file mode 100644 index 104e802849ebb177e64f568329f4baf9f3ee6736..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGGgAGU?ZmZ`8QY`6?zK#qG8~eHcB(eheoCO|{ z#S9F5he4R}c>anMpx`r47srr_TW>EnaxxfjFdyW2w|~=IrwK*ojvO`WGiA(*tKwE^ z{rmlWKc9kt00##P3lkG#V}pZ(f`R}D6B2@`L{Wty!~iu9Nfm|=1Kd`)Dx!qI&ONY* ZU8;{`!b*?e2S6V)c)I$ztaD0e0swM*L>K@7 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png deleted file mode 100644 index 2d6c2c25e5ea61957790fbb2f7d0bc3b72b1ec8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGGgAGU?ZmZ`8QY`6?zK#qG8~eHcB(eheoCO|{ z#S9F5he4R}c>anMpx{GK7srr_TW>EL@-i@RFdyXmu{|bag25fnjL(yoJnX$DJHLn1 zK|w)4fP;gDg^7u=vBAMX0fY$&F@RN~sKO9pfSQM-3R4JfD?$~fkRcf4v7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@f)buCjv*Ddl7F5*z~Iut$jv-4Aw;oMgoz>G3X{6>0?S&U8U{~S KKbLh*2~7aQ_8gl4 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png new file mode 100644 index 000000000..1909adfc3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc7901f3fbb29f3addd593406aafb867ba4c852629ad87ccc80f6e83181c4e44 +size 4609 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png new file mode 100644 index 000000000..ed9c7271d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0adfeb7a17cc4261701d41646216423c6cf066c0023cbd6b57571539fb333d83 +size 7641 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png new file mode 100644 index 000000000..6fb508f10 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:009a9e8035f3d1926bf7e25ed563564aec080e17472fd963df8d857f53233507 +size 7701 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png new file mode 100644 index 000000000..7eb5bc6d6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6160847478f64b019c0bdf32395b8700df0052313f35bfe952ad3b48c6671e9d +size 7634 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png new file mode 100644 index 000000000..ac2304484 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60147b29b6465755a6ed8147fc82e41510d4ada128263997b79094a4aaa4aca7 +size 4596 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png new file mode 100644 index 000000000..f9a9c52a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb62efe6efecb68e8897474cfc437b4f90dd254196128e91b6be2c11323200c8 +size 1425 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png new file mode 100644 index 000000000..8fb3dcbc6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d26fc5cd84771e18b05aaa72c80ee39567a339a86385be7c0c498a550b0e6f0 +size 1452 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png new file mode 100644 index 000000000..b56336ea8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5ae76b167965938f543cfc6f80bd4b6b1c016616882a475e32b7c1319a81e1 +size 1468 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png new file mode 100644 index 000000000..95c378570 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5c1044ad651b51aad6120355b235b3b04e984614fff53a498db000d92237f5c +size 1408 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..ee0dd5704 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 +size 130 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png new file mode 100644 index 000000000..ee0dd5704 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 +size 130 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..ee0dd5704 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 +size 130 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png new file mode 100644 index 000000000..12464c6fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c13059790e8e8a5024b9362c57503f4888675ccc8b64f0eefb865ce2e7002906 +size 172 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png new file mode 100644 index 000000000..f2d7da91b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f77b69a6935c2a3813777cef931cb9e1d735849baa9e56d75771f7493ecae76 +size 169 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png new file mode 100644 index 000000000..dddebf3da --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78bcdde736e2b0ff90a07405d596ba9f5dac7d4af67deb6f907d9117e79cbc96 +size 189 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png new file mode 100644 index 000000000..34978f1ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2c97732c0f92c328522b352fb81ec0671fc395b25d7d5ab9e2c336b040a819 +size 181 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png new file mode 100644 index 000000000..c512f35cb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03a87b74dd9a488e759864e75de78a23ba74ed93108027f839119e85583a5a74 +size 175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png new file mode 100644 index 000000000..bb599f236 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9963beae12b28e18942b4aad61c79494189116950432c53c6f99abe7485d2253 +size 1350 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/RotatedGradient.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushRotatedGradient.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/RotatedGradient.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushRotatedGradient.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png new file mode 100644 index 000000000..5f7077b41 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4ae9cbe72c9f83368a38edb158b373648ff92503c815ddea14bc434d60d1659 +size 217 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png new file mode 100644 index 000000000..c1be2838a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3ff0b6c04c39f5b1399e4781dede08176af01abfa16b7968dc11bce88beca09 +size 329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png new file mode 100644 index 000000000..1348e7857 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c866ee869ea057b4fccf70ec9fe9f91da9a67e1dcd0d40636fae4107a799e9f5 +size 324 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png new file mode 100644 index 000000000..c024c4904 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9d31234277dce14a01fea910aba703ef78982fa038d32ec56a2e0409ca4f1ff +size 319 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..1fd9d9708 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22fcdd48ddadb352d00032f9fc44076e5aad73964ea481860b6d45cfe848836c +size 118 From 52fb65f3ec747081cceb783cd51c387a18c9adc4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 12:50:59 +1000 Subject: [PATCH 053/136] Migrate FillsOutOfBounds tests --- .../Drawing/FillOutsideBoundsTests.cs | 57 ------------------ ...ithDrawingCanvasTests.FillOutsideBounds.cs | 53 ++++++++++++++++ ...cleOutsideBoundsDrawingArea_(-110_-20).png | Bin 141 -> 0 bytes ...cleOutsideBoundsDrawingArea_(-110_-49).png | Bin 141 -> 0 bytes ...cleOutsideBoundsDrawingArea_(-110_-50).png | Bin 141 -> 0 bytes ...cleOutsideBoundsDrawingArea_(-110_-60).png | Bin 141 -> 0 bytes ...ircleOutsideBoundsDrawingArea_(-110_0).png | Bin 141 -> 0 bytes ...CircleOutsideBoundsDrawingArea_(-99_0).png | Bin 141 -> 0 bytes ...CircleOutsideBoundsDrawingArea_(0_-50).png | Bin 141 -> 0 bytes ...CircleOutsideBoundsDrawingArea_(0_-60).png | Bin 141 -> 0 bytes ...rcleOutsideBoundsDrawingArea_(110_-49).png | Bin 153 -> 0 bytes ...rcleOutsideBoundsDrawingArea_(110_-50).png | Bin 141 -> 0 bytes ...rcleOutsideBoundsDrawingArea_(110_-60).png | Bin 141 -> 0 bytes ...cleOutsideBoundsDrawingArea_(-110_-20).png | 3 + ...cleOutsideBoundsDrawingArea_(-110_-49).png | 3 + ...cleOutsideBoundsDrawingArea_(-110_-50).png | 3 + ...cleOutsideBoundsDrawingArea_(-110_-60).png | 3 + ...ircleOutsideBoundsDrawingArea_(-110_0).png | 3 + ...CircleOutsideBoundsDrawingArea_(-99_0).png | 3 + ...ircleOutsideBoundsDrawingArea_(0_-20).png} | 0 ...ircleOutsideBoundsDrawingArea_(0_-49).png} | 0 ...CircleOutsideBoundsDrawingArea_(0_-50).png | 3 + ...CircleOutsideBoundsDrawingArea_(0_-60).png | 3 + ...wCircleOutsideBoundsDrawingArea_(0_0).png} | 0 ...cleOutsideBoundsDrawingArea_(110_-20).png} | 0 ...rcleOutsideBoundsDrawingArea_(110_-49).png | 3 + ...rcleOutsideBoundsDrawingArea_(110_-50).png | 3 + ...rcleOutsideBoundsDrawingArea_(110_-60).png | 3 + ...ircleOutsideBoundsDrawingArea_(110_0).png} | 0 ...CircleOutsideBoundsDrawingArea_(99_0).png} | 0 30 files changed, 86 insertions(+), 57 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-50).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-20).png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-49).png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png} (100%) rename tests/Images/ReferenceOutput/Drawing/{FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png => ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png} (100%) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs deleted file mode 100644 index bdbd43e06..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillOutsideBoundsTests -{ - [Theory] - [InlineData(-100)] // Crash - [InlineData(-99)] // Fine - [InlineData(99)] // Fine - [InlineData(100)] // Crash - public void DrawRectactangleOutsideBoundsDrawingArea(int xpos) - { - int width = 100; - int height = 100; - - using (Image image = new(width, height, Color.Red.ToPixel())) - { - Rectangle rectangle = new(xpos, 0, width, height); - - image.Mutate(x => x.Fill(Color.Black, rectangle)); - } - } - - public static TheoryData CircleCoordinates { get; } = new() - { - { -110, -60 }, { 0, -60 }, { 110, -60 }, - { -110, -50 }, { 0, -50 }, { 110, -50 }, - { -110, -49 }, { 0, -49 }, { 110, -49 }, - { -110, -20 }, { 0, -20 }, { 110, -20 }, - { -110, -50 }, { 0, -60 }, { 110, -60 }, - { -110, 0 }, { -99, 0 }, { 0, 0 }, { 99, 0 }, { 110, 0 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(CircleCoordinates), 100, 100, nameof(Color.Red), PixelTypes.Rgba32)] - public void DrawCircleOutsideBoundsDrawingArea(TestImageProvider provider, int xpos, int ypos) - { - int width = 100; - int height = 100; - - using Image image = provider.GetImage(); - EllipsePolygon circle = new(xpos, ypos, width, height); - - provider.RunValidatingProcessorTest( - x => x.Fill(Color.Black, circle), - $"({xpos}_{ypos})", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs new file mode 100644 index 000000000..4a0911c25 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static TheoryData FillOutsideBoundsCircleCoordinates { get; } = new() + { + { -110, -60 }, { 0, -60 }, { 110, -60 }, + { -110, -50 }, { 0, -50 }, { 110, -50 }, + { -110, -49 }, { 0, -49 }, { 110, -49 }, + { -110, -20 }, { 0, -20 }, { 110, -20 }, + { -110, -50 }, { 0, -60 }, { 110, -60 }, + { -110, 0 }, { -99, 0 }, { 0, 0 }, { 99, 0 }, { 110, 0 }, + }; + + [Theory] + [InlineData(-100)] + [InlineData(-99)] + [InlineData(99)] + [InlineData(100)] + public void FillOutsideBoundsDrawRectactangleOutsideBoundsDrawingArea(int xpos) + { + int width = 100; + int height = 100; + + using Image image = new(width, height, Color.Red.ToPixel()); + + Rectangle rectangle = new(xpos, 0, width, height); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(rectangle, Brushes.Solid(Color.Black)))); + } + + [Theory] + [WithSolidFilledImages(nameof(FillOutsideBoundsCircleCoordinates), 100, 100, nameof(Color.Red), PixelTypes.Rgba32)] + public void FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea(TestImageProvider provider, int xpos, int ypos) + { + int width = 100; + int height = 100; + + EllipsePolygon circle = new(xpos, ypos, width, height); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(circle, Brushes.Solid(Color.Black))), + $"({xpos}_{ypos})", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png deleted file mode 100644 index 04782a9bfc6496bfa633390df438e12eb589fe86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8TYyi9>pupD{|pQ!>0iBpA~K#Xjv*HQ$v^s8 rNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png deleted file mode 100644 index 06e28c3781724e68a7af996f63a051c86ac89d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%G}c0*}aI1_r*vAk26?e+5W8bAV5X>wg9Y$w!>#K(4T-i(`m{WU>V7;slYN dra*^q1_ss&2F5$COwvGU22WQ%mvv4FO#rxH9(n)( diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-20).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-20).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-49).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-49).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png new file mode 100644 index 000000000..f93de56a2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d631cd560df9f95e9e5bf19794c78b7f1598c0d0d80498991208d412ff4c88c3 +size 153 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png new file mode 100644 index 000000000..28c0a0bca --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 +size 141 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png From bb12d60789aab8802ad20b3a416e3122a32ae879 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 13:28:40 +1000 Subject: [PATCH 054/136] PathGradientBrush sampling & intersect fix; tests move --- .../Processing/PathGradientBrush.cs | 8 +- ...DrawingCanvasTests.PathGradientBrushes.cs} | 71 ++++++++---------- .../FillComplex.png | 3 - .../FillRectangleWithDifferentColors.png | Bin 187 -> 0 bytes ...eWithDifferentColors_Rgba32_Blank10x10.png | Bin 187 -> 0 bytes .../FillTriangleWithDifferentColors.png | Bin 729 -> 0 bytes .../FillTriangleWithDifferentColorsCenter.png | Bin 724 -> 0 bytes ...ifferentColorsCenter_Rgba32_Blank20x20.png | Bin 724 -> 0 bytes ...eWithDifferentColors_Rgba32_Blank20x20.png | Bin 729 -> 0 bytes .../FillTriangleWithGreyscale.png | Bin 424 -> 0 bytes ...gleWithGreyscale_HalfSingle_Blank20x20.png | Bin 424 -> 0 bytes .../FillWithCustomCenterColor.png | Bin 203 -> 0 bytes ...ithCustomCenterColor_Rgba32_Blank10x10.png | Bin 315 -> 0 bytes ...dRotateTheColorsWhenThereAreMorePoints.png | Bin 175 -> 0 bytes ...enThereAreMorePoints_Rgba32_Blank10x10.png | Bin 175 -> 0 bytes .../FillPathGradientBrushFillComplex.png | 3 + ...tBrushFillRectangleWithDifferentColors.png | 3 + ...eWithDifferentColors_Rgba32_Blank10x10.png | 3 + ...ntBrushFillTriangleWithDifferentColors.png | 3 + ...hFillTriangleWithDifferentColorsCenter.png | 3 + ...ifferentColorsCenter_Rgba32_Blank20x20.png | 3 + ...eWithDifferentColors_Rgba32_Blank20x20.png | 3 + ...GradientBrushFillTriangleWithGreyscale.png | 3 + ...gleWithGreyscale_HalfSingle_Blank20x20.png | 3 + ...GradientBrushFillWithCustomCenterColor.png | 3 + ...ithCustomCenterColor_Rgba32_Blank10x10.png | 3 + ...dRotateTheColorsWhenThereAreMorePoints.png | 3 + ...enThereAreMorePoints_Rgba32_Blank10x10.png | 3 + 28 files changed, 77 insertions(+), 44 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/FillPathGradientBrushTests.cs => Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs} (67%) delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors_Rgba32_Blank10x10.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors_Rgba32_Blank20x20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index e5a41196c..3e47f6c70 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -240,7 +240,8 @@ public PathGradientBrushApplicator( { get { - Vector2 point = new(x, y); + // Match other gradient brushes by evaluating at pixel centers. + Vector2 point = new(x + 0.5F, y + 0.5F); if (point == this.center) { @@ -345,7 +346,7 @@ protected override void Dispose(bool disposing) Vector2 ip = default; Vector2 closestIntersection = default; Edge? closestEdge = null; - const float minDistance = float.MaxValue; + float minDistance = float.MaxValue; foreach (Edge edge in this.edges) { if (!edge.Intersect(start, end, ref ip)) @@ -353,9 +354,10 @@ protected override void Dispose(bool disposing) continue; } - float d = Vector2.DistanceSquared(start, end); + float d = Vector2.DistanceSquared(start, ip); if (d < minDistance) { + minDistance = d; closestEdge = edge; closestIntersection = ip; } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs similarity index 67% rename from tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs index 15750e4ce..f51dee4a3 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs @@ -6,19 +6,18 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing/GradientBrushes")] -public class FillPathGradientBrushTests +public partial class ProcessWithDrawingCanvasTests { - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); + private static readonly ImageComparer PathGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01f); [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillRectangleWithDifferentColors(TestImageProvider provider) + public void FillPathGradientBrushFillRectangleWithDifferentColors(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( - TolerantComparer, + PathGradientTolerantComparer, image => { PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; @@ -26,16 +25,16 @@ public void FillRectangleWithDifferentColors(TestImageProvider p PathGradientBrush brush = new(points, colors); - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Theory] [WithBlankImage(20, 20, PixelTypes.Rgba32)] - public void FillTriangleWithDifferentColors(TestImageProvider provider) + public void FillPathGradientBrushFillTriangleWithDifferentColors(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( - TolerantComparer, + PathGradientTolerantComparer, image => { PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; @@ -43,13 +42,13 @@ public void FillTriangleWithDifferentColors(TestImageProvider pr PathGradientBrush brush = new(points, colors); - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Theory] [WithBlankImage(20, 20, PixelTypes.HalfSingle)] - public void FillTriangleWithGreyscale(TestImageProvider provider) + public void FillPathGradientBrushFillTriangleWithGreyscale(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( ImageComparer.TolerantPercentage(0.02f), @@ -65,16 +64,16 @@ public void FillTriangleWithGreyscale(TestImageProvider provider PathGradientBrush brush = new(points, colors); - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Theory] [WithBlankImage(20, 20, PixelTypes.Rgba32)] - public void FillTriangleWithDifferentColorsCenter(TestImageProvider provider) + public void FillPathGradientBrushFillTriangleWithDifferentColorsCenter(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( - TolerantComparer, + PathGradientTolerantComparer, image => { PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; @@ -82,34 +81,32 @@ public void FillTriangleWithDifferentColorsCenter(TestImageProvider x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillRectangleWithSingleColor(TestImageProvider provider) + public void FillPathGradientBrushFillRectangleWithSingleColor(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = [Color.Red]; + using Image image = provider.GetImage(); - PathGradientBrush brush = new(points, colors); + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = [Color.Red]; - image.Mutate(x => x.Fill(brush)); + PathGradientBrush brush = new(points, colors); - image.ComparePixelBufferTo(Color.Red); - } + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.ComparePixelBufferTo(Color.Red); } [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void ShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvider provider) + public void FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( - TolerantComparer, + PathGradientTolerantComparer, image => { PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; @@ -117,16 +114,16 @@ public void ShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvide PathGradientBrush brush = new(points, colors); - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillWithCustomCenterColor(TestImageProvider provider) + public void FillPathGradientBrushFillWithCustomCenterColor(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( - TolerantComparer, + PathGradientTolerantComparer, image => { PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; @@ -134,12 +131,12 @@ public void FillWithCustomCenterColor(TestImageProvider provider PathGradientBrush brush = new(points, colors, Color.White); - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); [Fact] - public void ShouldThrowArgumentNullExceptionWhenLinesAreNull() + public void FillPathGradientBrushShouldThrowArgumentNullExceptionWhenLinesAreNull() { Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; @@ -149,7 +146,7 @@ public void ShouldThrowArgumentNullExceptionWhenLinesAreNull() } [Fact] - public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven() + public void FillPathGradientBrushShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven() { PointF[] points = [new(0, 0), new(10, 0)]; Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; @@ -160,7 +157,7 @@ public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven() } [Fact] - public void ShouldThrowArgumentNullExceptionWhenColorsAreNull() + public void FillPathGradientBrushShouldThrowArgumentNullExceptionWhenColorsAreNull() { PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; @@ -170,10 +167,9 @@ public void ShouldThrowArgumentNullExceptionWhenColorsAreNull() } [Fact] - public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven() + public void FillPathGradientBrushShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven() { PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = []; PathGradientBrush Create() => new(points, colors, Color.White); @@ -183,7 +179,7 @@ public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven() [Theory] [WithBlankImage(100, 100, PixelTypes.Rgba32)] - public void FillComplex(TestImageProvider provider) + public void FillPathGradientBrushFillComplex(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( new TolerantImageComparer(0.2f), @@ -198,8 +194,7 @@ public void FillComplex(TestImageProvider provider) ]; PathGradientBrush brush = new(points, colors, Color.White); - - image.Mutate(x => x.Fill(brush)); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); }, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png deleted file mode 100644 index 145fba142..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73aba67441d29001460d8fab86ba1bc5623ec1196424cb7f30a0e074cdc52525 -size 9396 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png deleted file mode 100644 index 4f0b8eebe9fd51b587c8d86d1fe1608b7558139f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 187 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 MUHx3vIVCg!0KZ4D-2eap diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png deleted file mode 100644 index 6b6619a5135961e51ddfdd5c05e34fdcf4863bef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G}c0*}aI1_r*vAk26?e?yt#G_0&1BKN9LXmVuN|bLF0h2JJ||h!Q@U9AZAR)_ z#Sz<8D|_Yt9Adxsp(!hC`opZl?DZdd{r4OWukKv&P2lCNm=D}fb>BJP*fe(wFmxF_ MUHx3vIVCg!0KZ4D-2eap diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png deleted file mode 100644 index 68f88d380d7850dc427cf87f2629d54c1191eb81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@g1Me9jv*DdYWolJ9#G(L{eLjont|s6>#qMC>lB%9R38n|y`^X9C6LZ04Gp&b{3d?Sk7S8eR(3)|W>%0QX`Ul?{jE#>xkLzLE_AF=BRlojcS-+at xroC`%VO_!b(KK>LwcEOXb6Rg3U;DzY{$h*dn;W5N?LdneJYD@<);T3K0RW|pOCkUO diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png deleted file mode 100644 index 2f4392464778312084ee4b3c69c417c8cd93b641..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>VM%xNb!1@J z*w6hZkrl}2EbxddW?+2nVZqFi z!Fq&&P2d7o2P2P*gTDh$LIP_-!u#B`e>{`-u$)MG61>{XRQ8qBEH2&C%u(0xTq#+R zouS3j;QxVj>$LV=ZOfd*E@?gxf3fgwzr&p5|Z>9ITEgT@c0 zw;B5vv{o{jE;6t<5Ldule&O6dex^e4um)in)_Mo`?ZPF8c5wYZChf&)!EN?||4Y~7 z*cf4v7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@f`Ohcjv*DdTKgHf927a6)hjyR|2;qL_zP#1v@22^T%UcnGRv|( zn4P+LTco?NP>Am4zO~b%BwA Tw=DbzG@QZH)z4*}Q$iB}qJli< diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png deleted file mode 100644 index 87515e696d9e8dd00c8d137a395796dc4a96a9bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@f`Ohcjv*DdTKgHf927a6)hjyR|2;qL_zP#1v@22^T%UcnGRv|( zn4P+LTco?NP>Am4zO~b%BwA Tw=DbzG@QZH)z4*}Q$iB}qJli< diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png new file mode 100644 index 000000000..4d9445bc6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5d99aa86b5a87bdf2c893cd4d50747eedc8a85661b0ddbf38c7e027c2de0f73 +size 9163 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png new file mode 100644 index 000000000..2588ada26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9f72a01beefd90d9f82aca8654ae3001ccb39a66b5b4498a07996a988c74bd0 +size 384 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png new file mode 100644 index 000000000..2588ada26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9f72a01beefd90d9f82aca8654ae3001ccb39a66b5b4498a07996a988c74bd0 +size 384 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png new file mode 100644 index 000000000..738f02c6a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d0f7f9f291aca84d38ed3abdad5669bf82be1cb8f540f6a88bd8f0c10bc7aa5 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png new file mode 100644 index 000000000..48b5a132a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adc584e03db96076ed34a4f38b39352a39e4d215a8f780a5bcafd214525ce063 +size 788 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png new file mode 100644 index 000000000..48b5a132a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adc584e03db96076ed34a4f38b39352a39e4d215a8f780a5bcafd214525ce063 +size 788 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png new file mode 100644 index 000000000..738f02c6a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d0f7f9f291aca84d38ed3abdad5669bf82be1cb8f540f6a88bd8f0c10bc7aa5 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png new file mode 100644 index 000000000..c86a8c7f6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:612da10c5b7b402d71afbc84f7a712fe6371a7e36edaa564a369b60943ae4aed +size 397 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png new file mode 100644 index 000000000..c86a8c7f6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:612da10c5b7b402d71afbc84f7a712fe6371a7e36edaa564a369b60943ae4aed +size 397 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png new file mode 100644 index 000000000..bfa7f3be0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa5112662ee94a8fbee6c8e911bb513840993b3a3a43a346adb2dcace5b0e4be +size 334 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png new file mode 100644 index 000000000..bfa7f3be0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa5112662ee94a8fbee6c8e911bb513840993b3a3a43a346adb2dcace5b0e4be +size 334 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png new file mode 100644 index 000000000..b6abcd266 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b51ecc4260b3680bd22e42d4b5ecb84ecd37854d7654c05e58532ec2d715a82b +size 218 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png new file mode 100644 index 000000000..b6abcd266 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b51ecc4260b3680bd22e42d4b5ecb84ecd37854d7654c05e58532ec2d715a82b +size 218 From b63da784ac19fa51f095c13d4f4f50b66174e1a3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 13:34:51 +1000 Subject: [PATCH 055/136] Move FillPath tests to ProcessWithCanvas --- .../Drawing/FillPathTests.cs | 133 --------------- .../ProcessWithDrawingCanvasTests.FillPath.cs | 151 ++++++++++++++++++ .../FillPathArcToAlternates.png | 0 .../FillPathCanvasArcs.png | 0 .../FillPathSVGArcs.png | 0 5 files changed, 151 insertions(+), 133 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs rename tests/Images/ReferenceOutput/Drawing/{FillPathTests => ProcessWithDrawingCanvasTests}/FillPathArcToAlternates.png (100%) rename tests/Images/ReferenceOutput/Drawing/{FillPathTests => ProcessWithDrawingCanvasTests}/FillPathCanvasArcs.png (100%) rename tests/Images/ReferenceOutput/Drawing/{FillPathTests => ProcessWithDrawingCanvasTests}/FillPathSVGArcs.png (100%) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs deleted file mode 100644 index 1a6bcb5e9..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillPathTests -{ - // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths - [Theory] - [WithSolidFilledImages(325, 325, "White", PixelTypes.Rgba32)] - public void FillPathSVGArcs(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PathBuilder pb = new(); - - pb.MoveTo(new Vector2(80, 80)) - .ArcTo(45, 45, 0, false, false, new Vector2(125, 125)) - .LineTo(new Vector2(125, 80)) - .CloseFigure(); - - IPath path = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(230, 80)) - .ArcTo(45, 45, 0, true, false, new Vector2(275, 125)) - .LineTo(new Vector2(275, 80)) - .CloseFigure(); - - IPath path2 = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(80, 230)) - .ArcTo(45, 45, 0, false, true, new Vector2(125, 275)) - .LineTo(new Vector2(125, 230)) - .CloseFigure(); - - IPath path3 = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(230, 230)) - .ArcTo(45, 45, 0, true, true, new Vector2(275, 275)) - .LineTo(new Vector2(275, 230)) - .CloseFigure(); - - IPath path4 = pb.Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Fill(Color.Green, path).Fill(Color.Red, path2).Fill(Color.Purple, path3).Fill(Color.Blue, path4)), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - } - - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc - [Theory] - [WithSolidFilledImages(150, 200, "White", PixelTypes.Rgba32)] - public void FillPathCanvasArcs(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - ImageComparer.TolerantPercentage(5e-3f), - image => - { - for (int i = 0; i <= 3; i++) - { - for (int j = 0; j <= 2; j++) - { - PathBuilder pb = new(); - - float x = 25 + (j * 50); // x coordinate - float y = 25 + (i * 50); // y coordinate - float radius = 20; // Arc radius - float startAngle = 0; // Starting point on circle - float endAngle = 180F + (180F * j / 2F); // End point on circle - bool counterclockwise = i % 2 == 1; // Draw counterclockwise - - // To move counterclockwise we offset our sweepAngle parameter - // Canvas likely does something similar. - if (counterclockwise) - { - // 360 becomes zero and we don't accept that as a parameter (won't render). - if (endAngle < 360F) - { - endAngle = (360F - endAngle) % 360F; - } - - endAngle *= -1; - } - - pb.AddArc(x, y, radius, radius, 0, startAngle, endAngle); - - if (i > 1) - { - image.Mutate(x => x.Fill(Color.Black, pb.Build())); - } - else - { - image.Mutate(x => x.Draw(new SolidPen(Color.Black, 1), pb.Build())); - } - } - } - }, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - - [Theory] - [WithSolidFilledImages(400, 250, "White", PixelTypes.Rgba32)] - public void FillPathArcToAlternates(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - // Test alternate syntax. Both should overlap creating an orange arc. - PathBuilder pb = new(); - - pb.MoveTo(new Vector2(50, 50)); - pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200)); - IPath path = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(50, 50)); - pb.AddSegment(new ArcLineSegment(new Vector2(50, 50), new Vector2(200, 200), new SizeF(20, 50), -72F, true, true)); - IPath path2 = pb.Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Fill(Color.Yellow, path).Fill(Color.Red.WithAlpha(.5F), path2)), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs new file mode 100644 index 000000000..2d4375a02 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths + [Theory] + [WithSolidFilledImages(325, 325, "White", PixelTypes.Rgba32)] + public void FillPathSVGArcs(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PathBuilder pb = new(); + + pb.MoveTo(new Vector2(80, 80)) + .ArcTo(45, 45, 0, false, false, new Vector2(125, 125)) + .LineTo(new Vector2(125, 80)) + .CloseFigure(); + + IPath path = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(230, 80)) + .ArcTo(45, 45, 0, true, false, new Vector2(275, 125)) + .LineTo(new Vector2(275, 80)) + .CloseFigure(); + + IPath path2 = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(80, 230)) + .ArcTo(45, 45, 0, false, true, new Vector2(125, 275)) + .LineTo(new Vector2(125, 230)) + .CloseFigure(); + + IPath path3 = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(230, 230)) + .ArcTo(45, 45, 0, true, true, new Vector2(275, 275)) + .LineTo(new Vector2(275, 230)) + .CloseFigure(); + + IPath path4 = pb.Build(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(path, Brushes.Solid(Color.Green)); + canvas.Fill(path2, Brushes.Solid(Color.Red)); + canvas.Fill(path3, Brushes.Solid(Color.Purple)); + canvas.Fill(path4, Brushes.Solid(Color.Blue)); + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc + [Theory] + [WithSolidFilledImages(150, 200, "White", PixelTypes.Rgba32)] + public void FillPathCanvasArcs(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + for (int i = 0; i <= 3; i++) + { + for (int j = 0; j <= 2; j++) + { + PathBuilder pb = new(); + + float x = 25 + (j * 50); // x coordinate + float y = 25 + (i * 50); // y coordinate + float radius = 20; // Arc radius + float startAngle = 0; // Starting point on circle + float endAngle = 180F + (180F * j / 2F); // End point on circle + bool counterclockwise = i % 2 == 1; // Draw counterclockwise + + // To move counterclockwise we offset our sweepAngle parameter + // Canvas likely does something similar. + if (counterclockwise) + { + // 360 becomes zero and we don't accept that as a parameter (won't render). + if (endAngle < 360F) + { + endAngle = (360F - endAngle) % 360F; + } + + endAngle *= -1; + } + + pb.AddArc(x, y, radius, radius, 0, startAngle, endAngle); + + if (i > 1) + { + canvas.Fill(pb.Build(), Brushes.Solid(Color.Black)); + } + else + { + canvas.Draw(Pens.Solid(Color.Black, 1F), pb.Build()); + } + } + } + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(5e-3f), + provider, + appendSourceFileOrDescription: false, + appendPixelTypeToFileName: false); + } + + [Theory] + [WithSolidFilledImages(400, 250, "White", PixelTypes.Rgba32)] + public void FillPathArcToAlternates(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Test alternate syntax. Both should overlap creating an orange arc. + PathBuilder pb = new(); + + pb.MoveTo(new Vector2(50, 50)); + pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200)); + IPath path = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(50, 50)); + pb.AddSegment(new ArcLineSegment(new Vector2(50, 50), new Vector2(200, 200), new SizeF(20, 50), -72F, true, true)); + IPath path2 = pb.Build(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(path, Brushes.Solid(Color.Yellow)); + canvas.Fill(path2, Brushes.Solid(Color.Red.WithAlpha(.5F))); + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png From 7fa4871751da15c76c490b28f551d1c70cf5f04c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:03:11 +1000 Subject: [PATCH 056/136] Migrate text drawing tests to ProcessWithCanvas --- ImageSharp.Drawing.sln | 8 + .../ImageSharp.Drawing.csproj | 2 +- .../ProcessWithDrawingCanvasTests.Text.cs} | 183 +++++++++++++----- ...100_(0,0,0,255)_RichText-Path-(spiral).png | 0 ...0_(0,0,0,255)_RichText-Path-(triangle).png | 0 ...350_(0,0,0,255)_RichText-Path-(circle).png | 0 ...zontal_Rgba32_Blank100x100_type-spiral.png | 0 ...ntal_Rgba32_Blank120x120_type-triangle.png | 0 ...zontal_Rgba32_Blank350x350_type-circle.png | 0 ...ical_Rgba32_Blank250x250_type-triangle.png | 0 ...rtical_Rgba32_Blank350x350_type-circle.png | 0 ...anDrawTextVertical2_Rgba32_Blank48x935.png | 3 + ...wTextVerticalMixed2_Rgba32_Blank48x839.png | 3 + ...wTextVerticalMixed_Rgba32_Blank500x400.png | 3 + ...anDrawTextVertical_Rgba32_Blank500x400.png | 3 + ...lTextVerticalMixed_Rgba32_Blank500x400.png | 3 + ...anFillTextVertical_Rgba32_Blank500x400.png | 3 + .../CanRenderTextOutOfBoundsIssue301.png | 0 ...penSans-Regular.ttf)-S(32)-A(75)-Quic).png | 0 ...penSans-Regular.ttf)-S(40)-A(90)-Quic).png | 0 ...-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 3 + ...-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 3 + ...x200_(0,0,0,255)_RichText-Arabic-F(32).png | 0 ...x300_(0,0,0,255)_RichText-Arabic-F(40).png | 0 ...200_(0,0,0,255)_RichText-Rainbow-F(32).png | 0 ...300_(0,0,0,255)_RichText-Rainbow-F(40).png | 0 ...olid500x200_(0,0,0,255)_RichText-F(32).png | 0 ...olid500x300_(0,0,0,255)_RichText-F(40).png | 0 ...5,255,255,255)_ColorFontsEnabled-False.png | 0 ...55,255,255,255)_ColorFontsEnabled-True.png | 0 ..._Rgba32_Solid400x200_(255,255,255,255).png | 0 ...shBrushApplicatorIsThreadSafeIssue1044.png | 4 +- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 3 + ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 3 + ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 3 + ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 3 + ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 3 + ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 3 + ...ntShapesAreRenderedCorrectly_LargeText.png | 0 ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 0 ...)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 0 ...5,255)_OpenSans-Regular.ttf-50-i-(0,0).png | 0 ...55)_OpenSans-Regular.ttf-20-Sphi-(0,0).png | 0 ...55)_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 0 ...linespacing_1.5_linecount_3_wrap_False.png | 0 ..._linespacing_1.5_linecount_3_wrap_True.png | 0 ...g_linespacing_1_linecount_5_wrap_False.png | 0 ...ng_linespacing_1_linecount_5_wrap_True.png | 0 ...g_linespacing_2_linecount_2_wrap_False.png | 0 ...ng_linespacing_2_linecount_2_wrap_True.png | 0 ...egular.ttf)-S(50)-A(45)-Sphi-(550,550).png | 0 ...pleAB.woff)-S(50)-A(45)-ABAB-(100,100).png | 0 ...egular.ttf)-S(20)-A(45)-Sphi-(200,200).png | 0 ...ans-Regular.ttf)-S(50)-A(45)-i-(25,25).png | 0 ...ular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png | 0 ...eAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png | 0 ...lar.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png | 0 ...-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png | 0 ...gba32_Solid1000x1000_(255,255,255,255).png | 0 ...sitioningIsRobust_OpenSans-Regular.ttf.png | 0 ...anDrawTextVertical2_Rgba32_Blank48x935.png | 3 - ...wTextVerticalMixed2_Rgba32_Blank48x839.png | 3 - ...wTextVerticalMixed_Rgba32_Blank500x400.png | 3 - ...anDrawTextVertical_Rgba32_Blank500x400.png | 3 - ...lTextVerticalMixed_Rgba32_Blank500x400.png | 3 - ...anFillTextVertical_Rgba32_Blank500x400.png | 3 - ...-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 3 - ...-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 3 - ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 3 - ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 3 - ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 3 - ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 3 - ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 3 - ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 3 - 74 files changed, 185 insertions(+), 96 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/Text/DrawTextOnImageTests.cs => Processing/ProcessWithDrawingCanvasTests.Text.cs} (84%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanRenderTextOutOfBoundsIssue301.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_LargeText.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png (100%) rename tests/Images/ReferenceOutput/Drawing/{Text/DrawTextOnImageTests => ProcessWithDrawingCanvasTests}/TextPositioningIsRobust_OpenSans-Regular.ttf.png (100%) delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index c7e333c09..318aeae96 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -339,6 +339,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SixLabors.Fonts", "..\Fonts\src\SixLabors.Fonts\SixLabors.Fonts.csproj", "{4A922B77-34EC-EA6A-8E96-8353C8FA0640}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -365,6 +367,10 @@ Global {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -393,12 +399,14 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {4A922B77-34EC-EA6A-8E96-8353C8FA0640} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 + ..\Fonts\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{4a922b77-34ec-ea6a-8e96-8353c8fa0640}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection GlobalSection(Performance) = preSolution diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 89139a468..2eee14a22 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -50,7 +50,7 @@ - + diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs similarity index 84% rename from tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs index e7379712a..e06c7b184 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs @@ -10,13 +10,10 @@ using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Text; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing/Text")] -[ValidateDisposedMemoryAllocations] -public class DrawTextOnImageTests +public partial class ProcessWithDrawingCanvasTests { private const string AB = "AB\nAB"; @@ -26,11 +23,6 @@ public class DrawTextOnImageTests private static readonly ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(0.0069F); - public DrawTextOnImageTests(ITestOutputHelper output) - => this.Output = output; - - private ITestOutputHelper Output { get; } - [Theory] [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.ColrV0)] [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.None)] @@ -57,7 +49,7 @@ public void EmojiFontRendering(TestImageProvider provider, Color Origin = new PointF(img.Width / 2, img.Height / 2) }; - img.Mutate(i => i.DrawText(textOptions, text, color)); + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); }, $"ColorFontsEnabled-{colorFontSupport == ColorFontSupport.ColrV0}"); } @@ -89,7 +81,7 @@ public void FallbackFontRendering(TestImageProvider provider) Origin = new PointF(img.Width / 2, img.Height / 2) }; - img.Mutate(i => i.DrawText(textOptions, text, color)); + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); }); } @@ -120,7 +112,7 @@ public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688(TestIm Origin = new PointF(img.Width / 2, img.Height / 2) }; - img.Mutate(i => i.DrawText(textOptions, text, color)); + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); } [Theory] @@ -134,7 +126,11 @@ public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2(Test PointF point = new(100, 100); using Image img = provider.GetImage(); - img.Mutate(ctx => ctx.DrawText(text, font, color, point)); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, point), + text, + Brushes.Solid(color), + pen: null))); } [Theory] @@ -146,7 +142,11 @@ public void OpenSansJWithNoneZeroShouldntExtendPastGlyphe(TestImageProvi Color color = Color.Black; using Image img = provider.GetImage(); - img.Mutate(ctx => ctx.DrawText(TestText, font, Color.Black, new PointF(-50, 2))); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(-50, 2)), + TestText, + Brushes.Solid(Color.Black), + pen: null))); Assert.Equal(Color.White.ToPixel(), img[173, 2]); } @@ -169,7 +169,11 @@ public void FontShapesAreRenderedCorrectly( Font font = CreateFont(fontName, fontSize); provider.RunValidatingProcessorTest( - c => c.DrawText(text, font, Color.Black, new PointF(x, y)), + c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(x, y)), + text, + Brushes.Solid(Color.Black), + pen: null)), $"{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -204,10 +208,15 @@ public void FontShapesAreRenderedCorrectly_WithRotationApplied( Origin = new PointF(x, y) }; + Matrix3x2 transform = Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY)); + DrawingOptions drawingOptions = new() { Transform = transform }; + provider.RunValidatingProcessorTest( - x => x - .SetDrawingTransform(Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY))) - .DrawText(textOptions, text, Color.Black), + ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)}-({x},{y})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -244,10 +253,15 @@ public void FontShapesAreRenderedCorrectly_WithSkewApplied( Origin = new PointF(x, y) }; + Matrix3x2 transform = Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY)); + DrawingOptions drawingOptions = new() { Transform = transform }; + provider.RunValidatingProcessorTest( - x => x - .SetDrawingTransform(Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY))) - .DrawText(textOptions, text, Color.Black), + ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), $"F({fontName})-S({fontSize})-A({angleX},{angleY})-{ToTestOutputDisplayText(text)}-({x},{y})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -283,7 +297,11 @@ public void FontShapesAreRenderedCorrectly_LargeText( provider.VerifyOperation( comparer, - img => img.Mutate(c => c.DrawText(sb.ToString(), font, Color.Black, new PointF(10, 1))), + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(10, 1)), + sb.ToString(), + Brushes.Solid(Color.Black), + pen: null))), false, false); } @@ -333,7 +351,11 @@ public void FontShapesAreRenderedCorrectly_WithLineSpacing( provider.VerifyOperation( comparer, - img => img.Mutate(c => c.DrawText(textOptions, sb.ToString(), color)), + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + textOptions, + sb.ToString(), + Brushes.Solid(color), + pen: null))), $"linespacing_{lineSpacing}_linecount_{lineCount}_wrap_{wrap}", false, false); @@ -357,7 +379,11 @@ public void FontShapesAreRenderedCorrectlyWithAPen( provider.VerifyOperation( OutlinedTextDrawingComparer, - img => img.Mutate(c => c.DrawText(text, new Font(font, fontSize), Pens.Solid(color, 1), new PointF(x, y))), + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(new Font(font, fontSize), new PointF(x, y)), + text, + brush: null, + pen: Pens.Solid(color, 1)))), $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", appendPixelTypeToFileName: false, appendSourceFileOrDescription: true); @@ -381,7 +407,11 @@ public void FontShapesAreRenderedCorrectlyWithAPenPatterned( provider.VerifyOperation( OutlinedTextDrawingComparer, - img => img.Mutate(c => c.DrawText(text, new Font(font, fontSize), Pens.DashDot(color, 3), new PointF(x, y))), + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(new Font(font, fontSize), new PointF(x, y)), + text, + brush: null, + pen: Pens.DashDot(color, 3)))), $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", appendPixelTypeToFileName: false, appendSourceFileOrDescription: true); @@ -411,7 +441,7 @@ public void TextPositioningIsRobust(TestImageProvider provider, ImageComparer comparer = ImageComparer.TolerantPercentage(0.2f); provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.Black), + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null)), details, comparer, appendPixelTypeToFileName: false, @@ -430,11 +460,11 @@ public void CanDrawTextWithEmptyPath() Assert.NotEqual(FontRectangle.Empty, textSize); using Image image = new(Configuration.Default, (int)textSize.Width + 20, (int)textSize.Height + 20); - image.Mutate(x => x.DrawText( + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, Vector2.Zero), text, - font, - Color.Black, - Vector2.Zero)); + Brushes.Solid(Color.Black), + pen: null))); } [Theory] @@ -455,8 +485,13 @@ public void CanRotateFilledFont_Issue175( FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); + DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( - x => x.SetDrawingTransform(transform).DrawText(textOptions, text, Color.Black), + x => x.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -482,9 +517,13 @@ public void CanRotateOutlineFont_Issue175( FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); + DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( - x => x.SetDrawingTransform(transform) - .DrawText(textOptions, text, Pens.Solid(Color.Black, strokeWidth)), + x => x.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + brush: null, + pen: Pens.Solid(Color.Black, strokeWidth))), $"F({fontName})-S({fontSize})-A({angle})-STR({strokeWidth})-{ToTestOutputDisplayText(text)})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -556,7 +595,7 @@ public void DrawRichText( ] }; provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), $"RichText-F({fontSize})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -584,7 +623,7 @@ public void DrawRichTextArabic( ] }; provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), $"RichText-Arabic-F({fontSize})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -633,7 +672,7 @@ public void DrawRichTextRainbow( }; provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), $"RichText-Rainbow-F({fontSize})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -670,7 +709,7 @@ public void CanDrawRichTextAlongPathHorizontal(TestImageProvider }; provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), $"RichText-Path-({exampleImageKey})", TextDrawingComparer, appendPixelTypeToFileName: false, @@ -701,7 +740,12 @@ public void CanDrawTextAlongPathHorizontal(TestImageProvider pro IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 1, path).Fill(Color.Black, glyphs), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 1), path); + canvas.Fill(Brushes.Solid(Color.Black), glyphs); + }), new { type = exampleImageKey }, comparer: ImageComparer.TolerantPercentage(0.0025f)); } @@ -728,7 +772,12 @@ public void CanDrawTextAlongPathVertical(TestImageProvider provi IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 1, path).Fill(Color.Black, glyphs), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 1), path); + canvas.Fill(Brushes.Solid(Color.Black), glyphs); + }), new { type = exampleImageKey }, comparer: ImageComparer.TolerantPercentage(0.002f)); } @@ -775,10 +824,12 @@ public void PathAndTextDrawingMatch(TestImageProvider provider) IPathCollection tb = TextBuilder.GeneratePaths(text, path, to); - img.Mutate( - i => i.DrawLine(new SolidPen(Color.Red, 30), pathLine) - .DrawText(rto, text, Color.Black) - .Fill(Brushes.ForwardDiagonal(Color.HotPink), tb)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + canvas.DrawLine(new SolidPen(Color.Red, 30), pathLine); + canvas.DrawText(rto, text, Brushes.Solid(Color.Black), pen: null); + canvas.Fill(Brushes.ForwardDiagonal(Color.HotPink), tb); + })); } } }); @@ -806,7 +857,11 @@ public void CanFillTextVertical(TestImageProvider provider) IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, textOptions); provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Fill(Color.Black, glyphs), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawGlyphs(Brushes.Solid(Color.Black), Pens.Solid(Color.Black, 1F), glyphs); + }), comparer: ImageComparer.TolerantPercentage(0.002f)); } @@ -833,7 +888,11 @@ public void CanFillTextVerticalMixed(TestImageProvider provider) DrawingOptions options = new() { ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Fill(options, Color.Black, glyphs), + c => + { + c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White))); + c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.Black), glyphs)); + }, comparer: ImageComparer.TolerantPercentage(0.002f)); } @@ -858,7 +917,11 @@ public void CanDrawTextVertical(TestImageProvider provider) }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), comparer: ImageComparer.TolerantPercentage(0.002f)); } @@ -879,7 +942,11 @@ public void CanDrawTextVertical2(TestImageProvider provider) }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), comparer: ImageComparer.TolerantPercentage(0.002f)); } } @@ -903,7 +970,11 @@ public void CanDrawTextVerticalMixed(TestImageProvider provider) }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), comparer: ImageComparer.TolerantPercentage(0.002f)); } @@ -925,7 +996,11 @@ public void CanDrawTextVerticalMixed2(TestImageProvider provider }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), comparer: ImageComparer.TolerantPercentage(0.002f)); } } @@ -961,11 +1036,17 @@ public void CanRenderTextOutOfBoundsIssue301(TestImageProvider p new ColorStop(0.5f, Color.Yellow), new ColorStop(1f, Color.Blue)); - img.Mutate(m => m.DrawText(options, txt, brush)); + img.Mutate(m => m.ProcessWithCanvas(canvas => canvas.DrawText(options, txt, brush, pen: null))); }, false, false); + private static RichTextOptions CreateTextOptionsAt(Font font, PointF origin) + => new(font) { Origin = origin }; + + private static RichTextOptions CreateTextOptionsAt(Font font, Vector2 origin) + => new(font) { Origin = new PointF(origin.X, origin.Y) }; + private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times)); private static string ToTestOutputDisplayText(string text) diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png new file mode 100644 index 000000000..420694cd5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf1b1667add47bb8198f715dde96d703ede6932176515fcf0a677ebc1faac2c +size 15973 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png new file mode 100644 index 000000000..f664fcd7d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6139908f4611be49fbc2a71b8fa601377c7868a000c947c16f8201df76dfe54a +size 14353 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png new file mode 100644 index 000000000..c12ab5603 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0365bb7b03c33109f297808d875379b445aedc06050b331a2dea646c088e2f17 +size 32974 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png new file mode 100644 index 000000000..3de5762ff --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d8155ab8e62e55cdb2e1800cc1e5c9b2bcce859b7f768830c0c599817f066a3 +size 39354 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png new file mode 100644 index 000000000..7ead6c600 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbc4ff3438ca3f8aeba35886eb560818e69d5df494f11e7906834dcc827c0e30 +size 28075 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png new file mode 100644 index 000000000..e072cda80 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d104f6f6956e305c79d61685f2033e31095b6ca8a08d00c28d0e0209826b650 +size 20779 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png new file mode 100644 index 000000000..6424b742c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:456489c36d291f490650d75bbc02940df274a9739aa57b0dbcb0200613568ba8 +size 5878 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png new file mode 100644 index 000000000..30e9f56d0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c0082e83d7a7d7f6e3ae3d08ec9bb93e56b540e73fd5a2e55203067d174d9fd +size 6039 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png index ac2304484..382323e55 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60147b29b6465755a6ed8147fc82e41510d4ada128263997b79094a4aaa4aca7 -size 4596 +oid sha256:17c185a74dcde400c8e65585582669747eeeefd59e5af2322a62dd6c471a28f9 +size 92774 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png new file mode 100644 index 000000000..b34e8debb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec9e8c3a49d3c66fecec0bd653515028d67461d021c9f46dd919f275a943569b +size 31720 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png new file mode 100644 index 000000000..8953a1a8b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4209841a105ad355efd21e0e023a6f75fa38644f968b99d14e7c12f0ad5e7e5 +size 2822 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png new file mode 100644 index 000000000..70d555da1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea350bbc5712d5ed72d9942343e80e01417481e76274acc116ce379a76a83dba +size 29995 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png new file mode 100644 index 000000000..a3c036f79 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b568cca71399dd0184e9096a9b6cb64a19570f459909cf24ba9d73b4c99afc8a +size 28427 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png new file mode 100644 index 000000000..19242c796 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418ba58fbfd91d9249a1eaee7333272139988e07850c05c0d2ad54fcc48405ba +size 2407 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png new file mode 100644 index 000000000..2c8dd12ba --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9ea2fbe00d35143940184844d193c129e6aeb8e265ae597a805736ca8f89e30 +size 26685 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png deleted file mode 100644 index 1365aa909..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6dec4a6f836b95b35dd6b4bfefed4a139faf399f5ee0429d2af6da0d659ccf6b -size 4985 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png deleted file mode 100644 index 483091b77..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d3593b23fc0f52360731271313e444175efbbe5a3fe9df0e01422bb66cd311d -size 4906 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png deleted file mode 100644 index 95806e725..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe68e33222e02c38133a6555ec7aab8775ddac52e43e65ca08b9642587725237 -size 14318 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png deleted file mode 100644 index cb39952cb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7957a4f6299912762624320746e36a691f14a40f1282b3333d742e44e041e016 -size 13580 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png deleted file mode 100644 index ebebcb871..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfb920a3e19a7b6a86e7c16f26f370d91819100b1e9b38052781bdde9bc90078 -size 10593 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png deleted file mode 100644 index a9d95b2b6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb8c07ae7263cada6fde58146f84132c4fc725d18c96b699716bd468e3d0ae8a -size 5127 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png deleted file mode 100644 index 12024df67..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9bfb1deffe74cd385e005130793fcfaeade200ad6de77348c7624cb66d742204 -size 2582 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png deleted file mode 100644 index 9b8104f7c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80f7a935cc93f5bbc0fa9b02b2f36c294f71204f9654d224540cf69805f68f05 -size 2501 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png deleted file mode 100644 index 8dad5340a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6038e34918109e904806da6e70ada04a61db754784625b2572f75752fa521627 -size 17528 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png deleted file mode 100644 index 37e3bd5fb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a541428859171c4d2e0d23d63fc916aea2c3f911333886d6f61fcc198feb19b0 -size 759 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png deleted file mode 100644 index 0aa68114b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f6ec4b89aebe34fff668d656ff170ffee6c3a6b07d96eb3e414eb989bf21859 -size 16990 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png deleted file mode 100644 index 864ffbf1c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d618766c3826b46082f6c248205b51dc18e6f4f7a328f454cd085813ecb78a3c -size 15084 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png deleted file mode 100644 index 12ac94d02..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e26c9ceae90a42180b573f97da0ce2b12e4ef30b3043bcee014e24d227913be -size 706 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png deleted file mode 100644 index d839ae8e1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e91bd745be89a8d9126e5a9c73e0f62f286db3be7080545c80fef3ec19da177 -size 15452 From f01190b2814c336408440de6b496de8a96f1edc2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:12:51 +1000 Subject: [PATCH 057/136] Move FillPatternBrush tests to canvas tests --- .../Drawing/FillPatternBrushTests.cs | 302 ------------------ ...ssWithDrawingCanvasTests.PatternBrushes.cs | 285 +++++++++++++++++ .../BackwardDiagonal.png | Bin 140 -> 0 bytes .../BackwardDiagonal_Transparent.png | Bin 140 -> 0 bytes .../BackwardDiagonal_Transparentx4.png | Bin 329 -> 0 bytes .../BackwardDiagonalx4.png | Bin 335 -> 0 bytes .../FillPatternBrushTests/ForwardDiagonal.png | Bin 146 -> 0 bytes .../ForwardDiagonal_Transparent.png | Bin 143 -> 0 bytes .../ForwardDiagonal_Transparentx4.png | Bin 306 -> 0 bytes .../ForwardDiagonalx4.png | Bin 324 -> 0 bytes .../FillPatternBrushTests/Horizontal.png | Bin 123 -> 0 bytes .../Horizontal_Transparent.png | Bin 128 -> 0 bytes .../Horizontal_Transparentx4.png | Bin 238 -> 0 bytes .../FillPatternBrushTests/Horizontalx4.png | Bin 253 -> 0 bytes .../Drawing/FillPatternBrushTests/Min.png | Bin 123 -> 0 bytes .../FillPatternBrushTests/Min_Transparent.png | Bin 125 -> 0 bytes .../Min_Transparentx4.png | Bin 235 -> 0 bytes .../Drawing/FillPatternBrushTests/Minx4.png | Bin 250 -> 0 bytes .../FillPatternBrushTests/Percent10.png | Bin 137 -> 0 bytes .../Percent10_Transparent.png | Bin 133 -> 0 bytes .../Percent10_Transparentx4.png | Bin 282 -> 0 bytes .../FillPatternBrushTests/Percent10x4.png | Bin 292 -> 0 bytes .../FillPatternBrushTests/Percent20.png | Bin 127 -> 0 bytes .../Percent20_Transparent.png | Bin 130 -> 0 bytes .../Percent20_Transparentx4.png | Bin 280 -> 0 bytes .../FillPatternBrushTests/Percent20x4.png | Bin 285 -> 0 bytes .../FillPatternBrushTests/Vertical.png | Bin 121 -> 0 bytes .../Vertical_Transparent.png | Bin 119 -> 0 bytes .../Vertical_Transparentx4.png | Bin 224 -> 0 bytes .../FillPatternBrushTests/Verticalx4.png | Bin 227 -> 0 bytes ...houldBeFloodFilledWithBackwardDiagonal.png | 3 + ...dFilledWithBackwardDiagonalTransparent.png | 3 + ...ShouldBeFloodFilledWithForwardDiagonal.png | 3 + ...odFilledWithForwardDiagonalTransparent.png | 3 + ...ImageShouldBeFloodFilledWithHorizontal.png | 3 + ...BeFloodFilledWithHorizontalTransparent.png | 3 + ...rnBrushImageShouldBeFloodFilledWithMin.png | 3 + ...eShouldBeFloodFilledWithMinTransparent.png | 3 + ...hImageShouldBeFloodFilledWithPercent10.png | 3 + ...dBeFloodFilledWithPercent10Transparent.png | 3 + ...hImageShouldBeFloodFilledWithPercent20.png | 3 + ...dBeFloodFilledWithPercent20Transparent.png | 3 + ...shImageShouldBeFloodFilledWithVertical.png | 3 + ...ldBeFloodFilledWithVerticalTransparent.png | 3 + 44 files changed, 327 insertions(+), 302 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontalx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Minx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20x4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparentx4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Verticalx4.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs deleted file mode 100644 index 3fbba995f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -public class FillPatternBrushTests -{ - private void Test(string name, Color background, Brush brush, Color[,] expectedPattern) - { - string path = TestEnvironment.CreateOutputDirectory("Drawing", "FillPatternBrushTests"); - using (Image image = new(20, 20)) - { - image.Mutate(x => x.Fill(background).Fill(brush)); - - image.Save($"{path}/{name}.png"); - - Buffer2D sourcePixels = image.GetRootFramePixelBuffer(); - - // lets pick random spots to start checking - Random r = new(); - DenseMatrix expectedPatternFast = new(expectedPattern); - int xStride = expectedPatternFast.Columns; - int yStride = expectedPatternFast.Rows; - int offsetX = r.Next(image.Width / xStride) * xStride; - int offsetY = r.Next(image.Height / yStride) * yStride; - for (int x = 0; x < xStride; x++) - { - for (int y = 0; y < yStride; y++) - { - int actualX = x + offsetX; - int actualY = y + offsetY; - Rgba32 expected = expectedPatternFast[y, x].ToPixel(); // inverted pattern - Rgba32 actual = sourcePixels[actualX, actualY]; - if (expected != actual) - { - Assert.True(false, $"Expected {expected} but found {actual} at ({actualX},{actualY})"); - } - } - } - - image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); - image.Save($"{path}/{name}x4.png"); - } - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent10() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Percent10", - Color.Blue, - Brushes.Percent10(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent10Transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "Percent10_Transparent", - Color.Blue, - Brushes.Percent10(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent20() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } - }; - - Test( - "Percent20", - Color.Blue, - Brushes.Percent20(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent20_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } - }; - - Test( - "Percent20_Transparent", - Color.Blue, - Brushes.Percent20(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithHorizontal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Horizontal", - Color.Blue, - Brushes.Horizontal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithHorizontal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "Horizontal_Transparent", - Color.Blue, - Brushes.Horizontal(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithMin() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } - }; - - Test( - "Min", - Color.Blue, - Brushes.Min(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithMin_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - }; - - Test( - "Min_Transparent", - Color.Blue, - Brushes.Min(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithVertical() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Vertical", - Color.Blue, - Brushes.Vertical(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithVertical_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } - }; - - Test( - "Vertical_Transparent", - Color.Blue, - Brushes.Vertical(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithForwardDiagonal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "ForwardDiagonal", - Color.Blue, - Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.HotPink }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "ForwardDiagonal_Transparent", - Color.Blue, - Brushes.ForwardDiagonal(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithBackwardDiagonal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } - }; - - Test( - "BackwardDiagonal", - Color.Blue, - Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.HotPink } - }; - - Test( - "BackwardDiagonal_Transparent", - Color.Blue, - Brushes.BackwardDiagonal(Color.HotPink), - expectedPattern); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs new file mode 100644 index 000000000..43f43f17b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs @@ -0,0 +1,285 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent10(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent10(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent10(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent20(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent20(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent20(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithHorizontal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Horizontal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Horizontal(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithMin(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.Min(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithMinTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.Min(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithVertical(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Vertical(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Vertical(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.HotPink }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.ForwardDiagonal(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.BackwardDiagonal(Color.HotPink), expectedPattern); + } + + private static void VerifyFloodFillPattern( + TestImageProvider provider, + Brush brush, + Color[,] expectedPattern) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + ImageComparer.Exact, + image => + { + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.Blue)); + canvas.Fill(brush); + })); + + AssertPattern(image, expectedPattern); + }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + private static void AssertPattern(Image image, Color[,] expectedPattern) + where TPixel : unmanaged, IPixel + { + int rows = expectedPattern.GetLength(0); + int columns = expectedPattern.GetLength(1); + + TPixel[,] expectedPixels = new TPixel[rows, columns]; + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < columns; x++) + { + expectedPixels[y, x] = expectedPattern[y, x].ToPixel(); + } + } + + Buffer2D pixels = image.GetRootFramePixelBuffer(); + for (int y = 0; y < image.Height; y++) + { + Span row = pixels.DangerousGetRowSpan(y); + int patternY = y % rows; + for (int x = 0; x < image.Width; x++) + { + TPixel expected = expectedPixels[patternY, x % columns]; + Assert.Equal(expected, row[x]); + } + } + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png deleted file mode 100644 index a7ca5cb2aa9953f75075a13d72ac177e8e481735..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26twen zaSW-Lll*i3&Zqkey=0kr=FGi(#DQ}@>k&t>B~#8j@j6fVsbjD_=yZTsR+adhLl+HX gcn^b#g_3Oyb&-9ae$)xx2b#p->FVdQ&MBb@0GU}U6aWAK diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png deleted file mode 100644 index db76bec8ac4204355b1d01997d5c48c266e32aac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26twen zaSW-Lll*i3&ZqW|OY+$ye$3xFwNc29yQ5jhLvwzsRGY%*$OONo(-!Jn`J#8Db#bDF fR2!IZHs)b8XsOjnV|{ZKXcB{`tDnm{r-UW|oYN`y diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png deleted file mode 100644 index c3bd2318e0cf8e12025ead40d6d76f9719eb8407..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1G{> zi(^Q|t+zKe@*XzeaS4?CP&V_3aHU|no$69;#EucEBGY;<^e7awO?fk9Z(+Sd;kj|F$C`unFdEgGu7WP}rp)(rq zHO>&b3Nn7fw|EI*kj4Y&c#a&|8pi~bT=|~a;(&L9rEjMxFc=s-UHx3vIVCg!0K!a$ ATL1t6 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png deleted file mode 100644 index a1ed06f0b6a361ec1b87723812944fbbfdc66b2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fA0|O(Y zr;B4q#jUqDHu5qV3b+I+$GWzWk_)GbEAiTGE2@dzO1yRv9p78w&_u2Maz+ zTVQ)ZZp9&)&zyCrUSJ#0D)~aTBQp?U z#Q#>qiy)0}&N%qQyjBHD&Y0c!4XBRG_=2^- q2Fd0pdR{J+6N}~ly5Yf>c6LES-RdVQ*(N~q7(8A5T-G@yGywpm6)o5R diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png deleted file mode 100644 index f5a69702f639faa8347d6202d88f77b3998a85bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26m;}- zaSW-L^LD~UUIqo8!%b7(?`Y}XvE;j`XrbdtiTpU584hZ4JDHOG|5!}jSfrKAI(4IT n$gw{c_-DUsue(w3^Dwi3p>Fk$sEK=khB0`$`njxgN@xNAEm$pK diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png deleted file mode 100644 index d5e78ba98ebcdcca1b3feb4b30478fc9cbbcd901..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1FST zi(^Q|t+zKe^0qqgummb!l${zNI5pt@-yiB8GMp9jjoJO&=JA%Tm0Fy4=EnK`wU*nw z_QjvJkz!!1v2>VOUi+R|lP5vH!RY^roZhrs( diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png deleted file mode 100644 index 274f72b11edef7de15d2c2a687e62bca46b48f15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1F|l zi(^Q|t+zKe@*Z*!aJlGQYfj@M_O1 puuG~LuT+99_#*xctlOZ%?(qzZHHrC3KY<~@;OXk;vd$@?2>`2=dMp3{ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png deleted file mode 100644 index 482af03e7b17f942a8c9b8f5825a37e670f26d22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26x8)} zaSW-rm27C0{bhb*SHs8pXJW6UJaWZ183afM+&ys7K!*1)m{62zV|cV&=GFq6+z6mX N22WQ%mvv4FO#s09B8UJ0 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png deleted file mode 100644 index 4dcdf003843111283e1a048b684508a16b38fff1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26g2X5 zaSW-r_4Yy_Cxd~&;R`(H-mv(^pLeNa>um_S=g@THiCuC3Vtbc!@@o=#vm*}M^NRP` Wi5*v6{NDtqo59o7&t;ucLK6UZD8&btxar=cP~CK%fi6;PpaU3dlM@YOT^nE z_9z911_o9EAj!lbkidJv{sI#t6VG$`j7A1VCJ~?V*v)1ilTR4_v9^L zV`7=HxA9)16o-I<10$43c&neof5oAJ;k5Y$xfP8Jj7%a8P~zIZL*E>DSvWS-Fu$ct cQ-YCwMXT(o?whB(fKF%dboFyt=akR{06j!bEC2ui diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png deleted file mode 100644 index 79cf04240eb34815de03243c99f311ea0eac20db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26x8)} zaSW-rm27C0{bhb*S3_&#sfJf6B^Vg24@<3eX6e!ZYGm+q L^>bP0l+XkK+)N^3 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png deleted file mode 100644 index 1429926cbe09bdfbc18f56190d6d147833466da4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26x8>0 zaSW-r_4a}zPzlH30}~U(8ed3rN`1>={ZgoVlj|DSbonYXD;*xgBLdDm`y0tyGD7T8~4Vr1fZjwUWcrucUzfvuu@^w@*v0v*cW M>FVdQ&MBb@0Di zi(^Q|t+zJ z`x?JF@Un0SEMR~V8@_Sh1{rhU6Ym!GETCx&tT1A0{0*=%ZJ(Ky7E8~~b$qr2=xzp2 LS3j3^P6c)I$ztaD0e0suGEDLeoG diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png deleted file mode 100644 index c6d2a313257db93df038e3c79d0ff7ec8ab033f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26g2mA zaSW-rmHcb|&ZqXwhwCktO*zkz7{G6EP~yMw_C+2OPA}ibz4ED%gaq&5iv}`aVo@Ro ZgYLcsy%P?W#Q=?9@O1TaS?83{1OS8EC+YwI diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png deleted file mode 100644 index feff17b26c9b4d6beffbe43e1158b336ab23ede8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1FbW zi(^Q|t+zKe@-{f|FdtC(Tjujgcq7Ldag9u7)-H?h_vgr@7#6*g+^%z5YU&f4N9W(W zpIFVn@*{WJYwZaVCISi$4Q2Nxf0ugE!g#AuN`ry1jCITBKl~+!7~VQubZB5;yjA&? dF0y84Ik~yl@a9e3l@9b5gQu&X%Q~loCIFQaYk~j( diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png deleted file mode 100644 index 02efd39303fb475514a210ab38357dfddc090d34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1F_k zi(^Q|t+zKe@-`R@n*E_&~J$Zv$8kg={$3{P!@7HT?s-G@h^IXIH?CCJ2 zSI6W2)x>k}6>4CpTfOqDS;7Mc21b^E^VLP?8M_b Vg`i{Wr>j8C44$rjF6*2UngD99B#r<8 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png deleted file mode 100644 index 3522fd2aba4fa6b21d3e4d3568133d086f15842f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26g2U4 zaSW-Lll*i3&ZqW|OY+$ye$3xFwNc29yN7x5f+^>nbej|Y1d8|VD!9_RSWl)@u{VZ; YA%?SYw)>eNJ)nLDPgg&ebxsLQ0Qv$YyZ`_I diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png deleted file mode 100644 index 47614322ee9731dfc5591b0705207014d4580528..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9F5M?jcysy3fAQ1Fze zi(^Q|t+&@U@-`UoxCFYND)X7t8Nu?*c*+(PR+q}_yMKO~TX(iB#U|*R?md;;lG^vL zPguyn@nE~=cdjM9ulIj%)Z%$?NBRVF%AmpK!m*DIqHGh`Z+XbCIK=SDLHZm6Xgp1i>=%_ui&0mG5q_s&j#|Hb=UwcGTF`r=0mEjLbWT;%w`?yhyM}dD! zp-n(5S3ZQ8d*tr^@@(;9&8GdN{T|a)iMGxpT22WQ%mvv4FO#mEqW_tht diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png deleted file mode 100644 index 6f5d24ff9913d7903633bf4c551a6657e9ac4e26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26x8-~ zaSW-rm27C0{pJ6gIcjW=r&zH`END4@$RX7r>%oe~#cX|27FiDh3AL PwJ~_Q`njxgN@xNAY7QWs diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png deleted file mode 100644 index 9f1f581ed572d97bc13a315146c740a4744e6223..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`kN5fEmas?8@26x8%| zaSW-rm7J9D@`5w OF?hQAxvX{oKaCd&8eZnh2JH*M1c-u N@O1TaS?83{1OR_{G|2z} diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png new file mode 100644 index 000000000..bb6ada2b9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f0a9dd18ec12fbd464b370907bbeecbda5c31adebbe8ca8dbd2b98b8b0d01a3 +size 174 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png new file mode 100644 index 000000000..a372ec97b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c90be6cdc577df1fcaab8d689c552a8ce0b04e7d56ddf129f6cc0418f2c5cf48 +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png new file mode 100644 index 000000000..68dea434b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7c69d3ae3db0921b127c7858feee727692c02740b17ec147b59fa33d86fb776 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png new file mode 100644 index 000000000..38361a376 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7858ae798df8188dfee00652f00ec6f50b01ad4ae220a4d0d0a557adcc2c15f +size 179 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png new file mode 100644 index 000000000..d7037317f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81294e07eb5da65cb00944b6462c5c93d368e936cb38cdbf61a4930d21bb58be +size 184 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png new file mode 100644 index 000000000..e9c92bb55 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3ae1449dc56c7f8d460591cfb52d32220090809d24c2e28e58d251c1dce0b2a +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png new file mode 100644 index 000000000..50bca02ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bebc805cc88d273bc0da8c1df5fc3a75cfaaf42fe0143130090cf988d218f574 +size 181 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png new file mode 100644 index 000000000..501ddf10a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b209e3c08cde62127a97dd0c64bc3a0e6168d2803dfa2f16d809d671c567e1bd +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png new file mode 100644 index 000000000..f334102c4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86271e033299427d6ca6eb294abf248fc5b0567a9a32632d4e187e61c52ad700 +size 198 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png new file mode 100644 index 000000000..9823b0c1e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1d1d9d948ff823aa2da07b43140b763d35aa144bc053cc62f45305904bbc8d7 +size 198 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png new file mode 100644 index 000000000..428df318c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43d0bfe675fd1d4a142a164d3ad87dddfde0835b38a2ce4ab5bd602a7d8787b8 +size 167 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png new file mode 100644 index 000000000..7274161be --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ca932784c29726f0f1e519f74c1b59fe107e5ba7cad4146eedf0eb23b81cec2 +size 166 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png new file mode 100644 index 000000000..d86b89596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2f6a6f4d29f0aad8189679c12d5109ab0844d63d14231f4bce89db2ad97689 +size 164 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png new file mode 100644 index 000000000..197e0a11e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10585c68d57efbaa3faecb141f84fdfc9629415bec1591e1ee236afb7df6ecda +size 180 From 625ab8587a208eb3e980e10e048d74074d264142 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:33:47 +1000 Subject: [PATCH 058/136] Migrate FillPolygon tests to ProcessWithDrawingCanvas --- ...ProcessWithDrawingCanvasTests.Polygons.cs} | 303 +++++++++--------- ...verse(False)_IntersectionRule(EvenOdd).png | 3 - ...verse(False)_IntersectionRule(Nonzero).png | Bin 241 -> 0 bytes ...everse(True)_IntersectionRule(EvenOdd).png | 3 - ...everse(True)_IntersectionRule(Nonzero).png | 3 - .../FillPolygon_Concave_Reverse(False).png | Bin 266 -> 0 bytes .../FillPolygon_Concave_Reverse(True).png | Bin 298 -> 0 bytes ...olygon_ImageBrush_Rect_Rgba32_Car_rect.png | 3 - ...ygon_ImageBrush_Rect_Rgba32_ducky_rect.png | 3 - .../FillPolygon_ImageBrush_Rgba32_Car.png | 3 - .../FillPolygon_ImageBrush_Rgba32_ducky.png | 3 - .../FillPolygon_Pattern_Rgba32.png | 3 - .../FillPolygon_Solid_Basic_aa0.png | 3 - .../FillPolygon_Solid_Basic_aa16.png | 3 - .../FillPolygon_Solid_Basic_aa8.png | 3 - .../FillPolygon_Solid_Bgr24_Yellow_A1.png | 3 - .../FillPolygon_Solid_Rgba32_White_A0.6.png | 3 - .../FillPolygon_Solid_Rgba32_White_A1.png | 3 - ...ygon_Solid_Rgba32_White_A1_NoAntialias.png | 3 - ...sformed_Rgba32_BasicTestPattern250x350.png | 3 - .../FillPolygon_StarCircle.png | 3 - ...on_StarCircle_AllOperations_Difference.png | 3 - ..._StarCircle_AllOperations_Intersection.png | 3 - ...lPolygon_StarCircle_AllOperations_None.png | 3 - ...Polygon_StarCircle_AllOperations_Union.png | 3 - ...llPolygon_StarCircle_AllOperations_Xor.png | 3 - ...verse(False)_IntersectionRule(EvenOdd).png | 3 - ...verse(False)_IntersectionRule(Nonzero).png | 3 - ...everse(True)_IntersectionRule(EvenOdd).png | 3 - ...everse(True)_IntersectionRule(Nonzero).png | 3 - ...onzero_Rgba32_Solid60x60_(0,0,255,255).bmp | Bin 14454 -> 0 bytes ...ddEven_Rgba32_Solid60x60_(0,0,255,255).bmp | Bin 14454 -> 0 bytes .../Fill_RectangularPolygon_Rgba32.png | Bin 307 -> 0 bytes ...uration_Rgba32_BasicTestPattern100x100.png | 3 - ...sformed_Rgba32_BasicTestPattern100x100.png | 3 - .../Fill_RegularPolygon_V(3)_R(50)_Ang(0).png | 3 - ...ll_RegularPolygon_V(3)_R(60)_Ang(-180).png | 3 - ...Fill_RegularPolygon_V(3)_R(60)_Ang(20).png | 3 - .../Fill_RegularPolygon_V(5)_R(70)_Ang(0).png | 3 - ...ll_RegularPolygon_V(7)_R(80)_Ang(-180).png | 3 - ...verse(False)_IntersectionRule(EvenOdd).png | 3 + ...verse(False)_IntersectionRule(NonZero).png | 3 + ...everse(True)_IntersectionRule(EvenOdd).png | 3 + ...everse(True)_IntersectionRule(NonZero).png | 3 + .../FillPolygon_Concave_Reverse(False).png | 3 + .../FillPolygon_Concave_Reverse(True).png | 3 + ...verse(False)_IntersectionRule(EvenOdd).png | 3 + ...verse(False)_IntersectionRule(NonZero).png | 3 + ...everse(True)_IntersectionRule(EvenOdd).png | 3 + ...everse(True)_IntersectionRule(NonZero).png | 3 + ...olygon_ImageBrush_Rect_Rgba32_Car_rect.png | 3 + ...ygon_ImageBrush_Rect_Rgba32_ducky_rect.png | 3 + .../FillPolygon_ImageBrush_Rgba32_Car.png | 3 + .../FillPolygon_ImageBrush_Rgba32_ducky.png | 3 + ...onzero_Rgba32_Solid60x60_(0,0,255,255).bmp | 3 + ...ddEven_Rgba32_Solid60x60_(0,0,255,255).bmp | 3 + .../FillPolygon_Pattern_Rgba32.png | 3 + .../FillPolygon_RectangularPolygon_Rgba32.png | 3 + ...uration_Rgba32_BasicTestPattern100x100.png | 3 + ...sformed_Rgba32_BasicTestPattern100x100.png | 3 + ...lygon_RegularPolygon_V(3)_R(50)_Ang(0).png | 3 + ...on_RegularPolygon_V(3)_R(60)_Ang(-180).png | 3 + ...ygon_RegularPolygon_V(3)_R(60)_Ang(20).png | 3 + ...lygon_RegularPolygon_V(5)_R(70)_Ang(0).png | 3 + ...on_RegularPolygon_V(7)_R(80)_Ang(-180).png | 3 + .../FillPolygon_Solid_Basic_aa0.png | 3 + .../FillPolygon_Solid_Basic_aa16.png | 3 + .../FillPolygon_Solid_Basic_aa8.png | 3 + .../FillPolygon_Solid_Bgr24_Yellow_A1.png | 3 + .../FillPolygon_Solid_Rgba32_White_A0.6.png | 3 + .../FillPolygon_Solid_Rgba32_White_A1.png | 3 + ...ygon_Solid_Rgba32_White_A1_NoAntialias.png | 3 + ...sformed_Rgba32_BasicTestPattern250x350.png | 3 + .../FillPolygon_StarCircle.png | 3 + ...on_StarCircle_AllOperations_Difference.png | 3 + ..._StarCircle_AllOperations_Intersection.png | 3 + ...Polygon_StarCircle_AllOperations_Union.png | 3 + ...llPolygon_StarCircle_AllOperations_Xor.png | 3 + 78 files changed, 272 insertions(+), 244 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/FillPolygonTests.cs => Processing/ProcessWithDrawingCanvasTests.Polygons.cs} (59%) delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_None.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs similarity index 59% rename from tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs index 5739567e2..834ad6f61 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs @@ -7,11 +7,28 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing")] -public class FillPolygonTests +public partial class ProcessWithDrawingCanvasTests { + public static TheoryData FillPolygon_Complex_Data { get; } = + new() + { + { false, IntersectionRule.EvenOdd }, + { false, IntersectionRule.NonZero }, + { true, IntersectionRule.EvenOdd }, + { true, IntersectionRule.NonZero }, + }; + + public static readonly TheoryData FillPolygon_EllipsePolygon_Data = + new() + { + { false, IntersectionRule.EvenOdd }, + { false, IntersectionRule.NonZero }, + { true, IntersectionRule.EvenOdd }, + { true, IntersectionRule.NonZero }, + }; + [Theory] [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 0)] [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 8)] @@ -21,12 +38,19 @@ public void FillPolygon_Solid_Basic(TestImageProvider provider, { PointF[] polygon1 = PolygonFactory.CreatePointArray((2, 2), (6, 2), (6, 4), (2, 4)); PointF[] polygon2 = PolygonFactory.CreatePointArray((2, 8), (4, 6), (6, 8), (4, 10)); + Polygon shape1 = new(new LinearLineSegment(polygon1)); + Polygon shape2 = new(new LinearLineSegment(polygon2)); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias > 0 } + }; - GraphicsOptions options = new() { Antialias = antialias > 0 }; provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options) - .FillPolygon(Color.White, polygon1) - .FillPolygon(Color.White, polygon2), + c => c.ProcessWithCanvas(options, canvas => + { + canvas.Fill(shape1, Brushes.Solid(Color.White)); + canvas.Fill(shape2, Brushes.Solid(Color.White)); + }), testOutputDetails: $"aa{antialias}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -44,15 +68,18 @@ public void FillPolygon_Solid(TestImageProvider provider, string [ new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - - GraphicsOptions options = new() { Antialias = antialias }; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; string aa = antialias ? string.Empty : "_NoAntialias"; FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).FillPolygon(color, simplePath), + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(color))), outputDetails, appendSourceFileOrDescription: false); } @@ -66,45 +93,46 @@ public void FillPolygon_Solid_Transformed(TestImageProvider prov [ new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200)) + }; provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) - .FillPolygon(Color.White, simplePath)); + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); } [Theory] [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon_Solid_Transformed(TestImageProvider provider) + public void FillPolygon_RectangularPolygon_Solid_Transformed(TestImageProvider provider) where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(25, 25, 50, 50); + DrawingOptions options = new() + { + Transform = Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50)) + }; provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) - .Fill(Color.White, polygon)); + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); } [Theory] [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon_Solid_TransformedUsingConfiguration(TestImageProvider provider) + public void FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration(TestImageProvider provider) where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(25, 25, 50, 50); - provider.RunValidatingProcessorTest( - c => c - .SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) - .Fill(Color.White, polygon)); - } - - public static TheoryData FillPolygon_Complex_Data { get; } = - new() + DrawingOptions options = new() { - { false, IntersectionRule.EvenOdd }, - { false, IntersectionRule.NonZero }, - { true, IntersectionRule.EvenOdd }, - { true, IntersectionRule.NonZero }, + Transform = Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50)) }; + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); + } + [Theory] [WithBasicTestPatternImages(nameof(FillPolygon_Complex_Data), 100, 100, PixelTypes.Rgba32)] public void FillPolygon_Complex(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) @@ -123,15 +151,13 @@ public void FillPolygon_Complex(TestImageProvider provider, bool new Path(new LinearLineSegment(contour)), new Path(new LinearLineSegment(hole))); + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + }; + provider.RunValidatingProcessorTest( - c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - c.Fill(Color.White, polygon); - }, + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White))), testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, @@ -147,21 +173,22 @@ public void FillPolygon_Concave(TestImageProvider provider, bool PointF[] points = [ new Vector2(8, 8), - new Vector2(64, 8), - new Vector2(64, 64), - new Vector2(120, 64), - new Vector2(120, 120), - new Vector2(8, 120) + new Vector2(64, 8), + new Vector2(64, 64), + new Vector2(120, 64), + new Vector2(120, 120), + new Vector2(8, 120) ]; if (reverse) { Array.Reverse(points); } + Polygon polygon = new(new LinearLineSegment(points)); Color color = Color.LightGreen; provider.RunValidatingProcessorTest( - c => c.FillPolygon(color, points), + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), testOutputDetails: $"Reverse({reverse})", comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, @@ -177,7 +204,7 @@ public void FillPolygon_StarCircle(TestImageProvider provider) IPath shape = circle.Clip(star); provider.RunValidatingProcessorTest( - c => c.Fill(Color.White, shape), + c => c.ProcessWithCanvas(canvas => canvas.Fill(shape, Brushes.Solid(Color.White))), comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -194,11 +221,17 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi Star star = new(64, 64, 5, 24, 64); // See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference. - ShapeOptions options = new() { BooleanOperation = operation }; - IPath shape = star.Clip(options, circle); + ShapeOptions shapeOptions = new() { BooleanOperation = operation }; + IPath shape = star.Clip(shapeOptions, circle); + DrawingOptions options = new() { ShapeOptions = shapeOptions }; provider.RunValidatingProcessorTest( - c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), + c => c.ProcessWithCanvas(options, canvas => + { + canvas.Fill(circle, Brushes.Solid(Color.DeepPink)); + canvas.Fill(star, Brushes.Solid(Color.LightGray)); + canvas.Fill(shape, Brushes.Solid(Color.ForestGreen)); + }), testOutputDetails: operation.ToString(), comparer: ImageComparer.TolerantPercentage(0.01F), appendPixelTypeToFileName: false, @@ -214,12 +247,11 @@ public void FillPolygon_Pattern(TestImageProvider provider) [ new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) ]; - Color color = Color.Yellow; - - PatternBrush brush = Brushes.Horizontal(color); + Polygon polygon = new(new LinearLineSegment(simplePath)); + PatternBrush brush = Brushes.Horizontal(Color.Yellow); provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), appendSourceFileOrDescription: false); } @@ -233,16 +265,15 @@ public void FillPolygon_ImageBrush(TestImageProvider provider, s [ new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); - using (Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes)) - { - ImageBrush brush = new(brushImage); + using Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes); + ImageBrush brush = new(brushImage); - provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), - System.IO.Path.GetFileNameWithoutExtension(brushImageName), - appendSourceFileOrDescription: false); - } + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), + System.IO.Path.GetFileNameWithoutExtension(brushImageName), + appendSourceFileOrDescription: false); } [Theory] @@ -255,33 +286,33 @@ public void FillPolygon_ImageBrush_Rect(TestImageProvider provid [ new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); - using (Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes)) - { - float top = brushImage.Height / 4; - float left = brushImage.Width / 4; - float height = top * 2; - float width = left * 2; + using Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes); - ImageBrush brush = new(brushImage, new RectangleF(left, top, width, height)); + float top = brushImage.Height / 4F; + float left = brushImage.Width / 4F; + float height = top * 2; + float width = left * 2; - provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), - System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect", - appendSourceFileOrDescription: false); - } + ImageBrush brush = new(brushImage, new RectangleF(left, top, width, height)); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), + System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect", + appendSourceFileOrDescription: false); } [Theory] [WithBasicTestPatternImages(250, 250, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon(TestImageProvider provider) + public void FillPolygon_RectangularPolygon(TestImageProvider provider) where TPixel : unmanaged, IPixel { RectangularPolygon polygon = new(10, 10, 190, 140); Color color = Color.White; provider.RunValidatingProcessorTest( - c => c.Fill(color, polygon), + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), appendSourceFileOrDescription: false); } @@ -291,7 +322,7 @@ public void Fill_RectangularPolygon(TestImageProvider provider) [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, -180f)] [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 5, 70, 0f)] [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 7, 80, -180f)] - public void Fill_RegularPolygon(TestImageProvider provider, int vertices, float radius, float angleDeg) + public void FillPolygon_RegularPolygon(TestImageProvider provider, int vertices, float radius, float angleDeg) where TPixel : unmanaged, IPixel { float angle = GeometryUtilities.DegreeToRadian(angleDeg); @@ -300,24 +331,15 @@ public void Fill_RegularPolygon(TestImageProvider provider, int FormattableString testOutput = $"V({vertices})_R({radius})_Ang({angleDeg})"; provider.RunValidatingProcessorTest( - c => c.Fill(color, polygon), + c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), testOutput, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - public static readonly TheoryData Fill_EllipsePolygon_Data = - new() - { - { false, IntersectionRule.EvenOdd }, - { false, IntersectionRule.NonZero }, - { true, IntersectionRule.EvenOdd }, - { true, IntersectionRule.NonZero }, - }; - [Theory] - [WithBasicTestPatternImages(nameof(Fill_EllipsePolygon_Data), 200, 200, PixelTypes.Rgba32)] - public void Fill_EllipsePolygon(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) + [WithBasicTestPatternImages(nameof(FillPolygon_EllipsePolygon_Data), 200, 200, PixelTypes.Rgba32)] + public void FillPolygon_EllipsePolygon(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) where TPixel : unmanaged, IPixel { IPath polygon = new EllipsePolygon(100, 100, 80, 120); @@ -327,16 +349,13 @@ public void Fill_EllipsePolygon(TestImageProvider provider, bool } Color color = Color.Azure; + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + }; provider.RunValidatingProcessorTest( - c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - c.Fill(color, polygon); - }, + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(color))), testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -344,68 +363,62 @@ public void Fill_EllipsePolygon(TestImageProvider provider, bool [Theory] [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] - public void Fill_IntersectionRules_OddEven(TestImageProvider provider) + public void FillPolygon_IntersectionRules_OddEven(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image img = provider.GetImage()) + using Image img = provider.GetImage(); + Polygon poly = new(new LinearLineSegment( + new PointF(10, 30), + new PointF(10, 20), + new PointF(50, 20), + new PointF(50, 50), + new PointF(20, 50), + new PointF(20, 10), + new PointF(30, 10), + new PointF(30, 40), + new PointF(40, 40), + new PointF(40, 30), + new PointF(10, 30))); + + DrawingOptions options = new() { - Polygon poly = new(new LinearLineSegment( - new PointF(10, 30), - new PointF(10, 20), - new PointF(50, 20), - new PointF(50, 50), - new PointF(20, 50), - new PointF(20, 10), - new PointF(30, 10), - new PointF(30, 40), - new PointF(40, 40), - new PointF(40, 30), - new PointF(10, 30))); - - img.Mutate(c => c.Fill( - new DrawingOptions - { - ShapeOptions = { IntersectionRule = IntersectionRule.EvenOdd }, - }, - Color.HotPink, - poly)); - - provider.Utility.SaveTestOutputFile(img); - - Assert.Equal(Color.Blue.ToPixel(), img[25, 25]); - } + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + img.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(poly, Brushes.Solid(Color.HotPink)))); + + provider.Utility.SaveTestOutputFile(img); + Assert.Equal(Color.Blue.ToPixel(), img[25, 25]); } [Theory] [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] - public void Fill_IntersectionRules_Nonzero(TestImageProvider provider) + public void FillPolygon_IntersectionRules_Nonzero(TestImageProvider provider) where TPixel : unmanaged, IPixel { Configuration.Default.MaxDegreeOfParallelism = 1; - using (Image img = provider.GetImage()) + using Image img = provider.GetImage(); + Polygon poly = new(new LinearLineSegment( + new PointF(10, 30), + new PointF(10, 20), + new PointF(50, 20), + new PointF(50, 50), + new PointF(20, 50), + new PointF(20, 10), + new PointF(30, 10), + new PointF(30, 40), + new PointF(40, 40), + new PointF(40, 30), + new PointF(10, 30))); + + DrawingOptions options = new() { - Polygon poly = new(new LinearLineSegment( - new PointF(10, 30), - new PointF(10, 20), - new PointF(50, 20), - new PointF(50, 50), - new PointF(20, 50), - new PointF(20, 10), - new PointF(30, 10), - new PointF(30, 40), - new PointF(40, 40), - new PointF(40, 30), - new PointF(10, 30))); - img.Mutate(c => c.Fill( - new DrawingOptions - { - ShapeOptions = { IntersectionRule = IntersectionRule.NonZero }, - }, - Color.HotPink, - poly)); - - provider.Utility.SaveTestOutputFile(img); - Assert.Equal(Color.HotPink.ToPixel(), img[25, 25]); - } + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } + }; + + img.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(poly, Brushes.Solid(Color.HotPink)))); + + provider.Utility.SaveTestOutputFile(img); + Assert.Equal(Color.HotPink.ToPixel(), img[25, 25]); } } diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png deleted file mode 100644 index 36bc63aa0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5c1275eca572b8121e7f1f3a2f0ebee9684c0f12e85ad32dd9ab11aa1c8bfc9d -size 241 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png deleted file mode 100644 index 02818fe19ba39e91c22236ae3541984ba51d4435..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tA$G(Ey(i*Z&Ov8GxvPL7+nb#1atr|Np;0!3uMb zktIQX!3}HUqTPU;2u~NskO=p;ryY3@81S$hTpYl)5R4x3D{hkC8geyo-^Q}~Z<$v% zM@>@@@X$1x=BO~?*OQHT*|UGVG=8E`wD;(n Vt+t1j^aJf+@O1TaS?83{1OW2nQqTYZ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png deleted file mode 100644 index d029a873a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 -size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png deleted file mode 100644 index d029a873a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 -size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png deleted file mode 100644 index ff75d76845e18437293576b5faa51bfd5ed08ea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5m8A!&LZC(qc7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@g8TtKA+G-!{xbkk1H*)O6M(GHsadx{hLr^Q z1vjjXi*^HY$~|2iLn>~)J?kjQARxeSVD*9~F0B{yJ6VqXoRPTwfSdp7OvQ;d6SlB+ zx+qQbP!U3+{!CqiB=F?z5})OsZzh3-id0W_+8`-KQU@~TpBJ0Sj31pJ7Z^?gI)TB{ L)z4*}Q$iB}BCuCq diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png deleted file mode 100644 index 7b75147f4dc4897680ce9dd73e71e9f5c2e89462..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5m8A!&LZC(qc7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@f)W8fA+G-!{xbkk14H}s_6hGMOuyg$gP#*9 z#B?=zC&<*2Aiv;-wQzGt5$M9!Dn zvAmu```ov6`m9yiwQuid*p$-O_o=1Stn5KLkPf5+=|DP=4x|Ii?ZEw7t7Ex3;b85V z;hpCWeTb8{U`_3spnJsQ4Xq8&!_gI98{;CPwhuJs7^hf+K+NP*4s)JosW2R4fUfO zR7Z1NI?6$HE)JSYIjD~2x^$F->RcQ&mvT@Y&2{N02i3VaXfEZTI-2X!Q4XqeanM}K zL3K3OrK22F=i;EbltZms`h4LX_Z}kW`}_ZPb+250Ip3Ub?|`d+>fH1>eg3CDtM5nr X#{C}3{$>Btfpj1pNC(n^`8)6g4|-%t diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp deleted file mode 100644 index 7c1f285701d0384bc84767ddde1c29d2dd445901..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14454 zcmeI1A&vq;6hs?^BOGBRCm;wEo=f1kz@7|)=RSB$GYzamyo5K?WK)y;4dSKh*K~D4 zmfik$Qy$NH_`4cr`IZc_!~PcOd6G&3#Uv)8{cdko%kaJ4Oe_=qk~qS%_9+Y(45rIeR}_kLIA9y`QZ| zb5PFS&(@(LyPv-h+0Xb#HR``LOl2j%SjY(1KTa`t|<9?fx^FDsez;abTM zt;Y1qYx`0?%F%P9ob5~XC`WTp&i18xl%qK)XZun;%F!H@vwf)^Z?QFYBgn9lAiv;- zwQ@-{f|ux{{t5b3}aabW+l-~;@pJrve9Ub|WJMmKf)?YH{x zuIuc+n+If-5lz+KoUC%V`tByU9{Kz7w^tn}+K}3u`0s};!VUkbrUBi`;OXk;vd$@? F2>=V7k3IkZ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png deleted file mode 100644 index 7a4342074..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e -size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png deleted file mode 100644 index 7a4342074..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e -size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png deleted file mode 100644 index 15431f30a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02891cbdc2242395343290bc5403fd161fab49e470f93fd0c5639a93464f274b -size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png deleted file mode 100644 index 4e29cc25b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f95be339c0fc7f9315968001722777d1eebddbf6eea41c8d9d524b8775842763 -size 2024 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png deleted file mode 100644 index 3fe215ed7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86280439d207ed0a74595757af265a313d75f7ff8b7502eb17d2d7184855eb12 -size 2499 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png deleted file mode 100644 index 8ad422f6d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d762246aeec860558a8ee8e5318126937be11a9561a4bd1ff18f90900858bc2b -size 2852 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png deleted file mode 100644 index c7cb00188..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20457c79f2f5a782088bc4f9a333d0092ba9c5f5307835cdfc3ca7bf527420e4 -size 3247 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..666fead72 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a +size 753 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..666fead72 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a +size 753 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..666fead72 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a +size 753 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..666fead72 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a +size 753 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png new file mode 100644 index 000000000..0f374410e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f095e7c1a78e5ae92c053aa4b1bc3b2469486f6650fb21dc0957b55ba852137 +size 1710 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png new file mode 100644 index 000000000..0f374410e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f095e7c1a78e5ae92c053aa4b1bc3b2469486f6650fb21dc0957b55ba852137 +size 1710 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..9f0037f8a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb +size 3078 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..9f0037f8a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb +size 3078 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..9f0037f8a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb +size 3078 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..9f0037f8a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb +size 3078 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png new file mode 100644 index 000000000..c878a2d63 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3e5dd912b09cec622f2c751016d687c3b7fa8edfb4387176675c0bc4f5df49e +size 48115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png new file mode 100644 index 000000000..c7c963e0f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b8d3817418cb2eb0700975763f143c3a23db87e32e294836b409080c7fcdf75 +size 30251 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png new file mode 100644 index 000000000..fa919bbd6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69cb1264e59b110ec51afeeeeb3f24d5c314c4b3c9d6b1c1221ba066a6d22afc +size 16292 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png new file mode 100644 index 000000000..5a7223563 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec49fc14608b92be4b2f11bb5988ba36e0e7b55ccdaab6643757ac66fa7c56df +size 25167 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp new file mode 100644 index 000000000..e191ef63d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba74a337d12292afa1be49e6dd7684c1b2302142c3e6f127e6d0a9ca68490a29 +size 14454 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp new file mode 100644 index 000000000..4c06a02b9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f9b227887ea996980662411698876098dee5c5533713b4e499835437f0cd86 +size 14454 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png new file mode 100644 index 000000000..8ab2e0750 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2bb73046097700066001f12d682aee690cd8e82e8c0844195381b9337703711 +size 3219 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png new file mode 100644 index 000000000..81a10570f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c69974c77c9265f9af9eb93afa38ac741b4f7ed56aa0766d10070691ed13ce02 +size 1925 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png new file mode 100644 index 000000000..ab6221927 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9500a506cf668803bc42bdc34fecbfaa3210ddc5f49f05e0ce94228668a0788a +size 866 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png new file mode 100644 index 000000000..ab6221927 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9500a506cf668803bc42bdc34fecbfaa3210ddc5f49f05e0ce94228668a0788a +size 866 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png new file mode 100644 index 000000000..ce8a6b4e4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:024dab2c1b0a56059148e8e1bbd9f3e9deabbde452f2edc6350d8b81cf115f12 +size 2484 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png new file mode 100644 index 000000000..b54d96c7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86fb466875c45eca3a2d781934aea6773d1ffae6c885e6934af41de6e4b1ebca +size 2574 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png new file mode 100644 index 000000000..61319dd4e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:981e3fac81d73c81dd2f7d9a410902f20dd73edc68af533aed35aa0f1aee1e96 +size 3001 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png new file mode 100644 index 000000000..722981dbb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:279afb7b9ad0144bda9d0d9eb5b19dbe29ef2969ea5821dc9e6473b6ff9ffec8 +size 3196 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png new file mode 100644 index 000000000..5080ee1f1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ed7f179e3be4fa87c2196927ad9f32798a25a6aa05e22eae5c00eefa00e36c +size 3521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png new file mode 100644 index 000000000..fbb6127cb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:381e1c8eba1a3970e6f5c01f841bcf77a200ca501982fec759c381cd3c708ddb +size 154 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png new file mode 100644 index 000000000..66151f9b3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:774e1d9632c69630c151288b6b637d046ad72d57a3017929344c17ceb2d5c621 +size 160 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png new file mode 100644 index 000000000..66151f9b3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:774e1d9632c69630c151288b6b637d046ad72d57a3017929344c17ceb2d5c621 +size 160 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png new file mode 100644 index 000000000..a12766270 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b210f8412d1be85464b7038f8b92a360347473598016aebdbc90e40e4effba28 +size 4690 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png new file mode 100644 index 000000000..a4287c105 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ec9b036a0e9e685e97fea67f1efc2ec310ffc8956aa78ddcb462b3cc22b366 +size 4874 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png new file mode 100644 index 000000000..46efe9db1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92fc9b26b79772356b9bae39b9d8903add17da7a483018947614426c6b693de5 +size 4680 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png new file mode 100644 index 000000000..d39599e36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b0c338157679f1e84ebfcb4e0f113e699e9157e67522d4a6d9d19f681b4fed5 +size 3377 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png new file mode 100644 index 000000000..c10fe1374 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c85cf1e911143fa2d4f54a8bc8922021d5961b96d257137cfa21f2e5aec00eb2 +size 6808 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png new file mode 100644 index 000000000..83cca9694 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634e8133e16ae16f66bad2e95b109d99af8b82dc32a8b4f9e23af81282d18922 +size 2083 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png new file mode 100644 index 000000000..d78c4a208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d7bcb3676267de117078593f60bfa4ce675a2c07a5cb0979bb9faf80a2f4afc +size 3618 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png new file mode 100644 index 000000000..2136151b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d74bc3ac9a9b374e769473abaa652bb0976db262b5e409ab9da8021c3bfd4edb +size 3738 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png new file mode 100644 index 000000000..5420e25e2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1902d6dedd1b8ee99ed7f6617b8226559c1c4dca9b76b52f90fe7882a00edf21 +size 2455 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png new file mode 100644 index 000000000..36e469dc1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9138adc1bdbc42b7542300bbf9e7bf518de10e9f2e5e145be33135ca2cc41eb +size 3617 From 34468f374accf7c95abbd021940ae45766f29592 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:39:30 +1000 Subject: [PATCH 059/136] Migrate RadialGradient tests --- .../Drawing/FillRadialGradientBrushTests.cs | 73 ------------------ ...sWithDrawingCanvasTests.GradientBrushes.cs | 52 +++++++++++++ ...entCentersReturnsImage_center(-40,100).png | Bin 2112 -> 0 bytes ...fferentCentersReturnsImage_center(0,0).png | Bin 1888 -> 0 bytes ...erentCentersReturnsImage_center(0,100).png | Bin 3385 -> 0 bytes ...erentCentersReturnsImage_center(100,0).png | Bin 3405 -> 0 bytes ...entCentersReturnsImage_center(100,100).png | Bin 5915 -> 0 bytes .../WithEqualColorsReturnsUnicolorImage.png | Bin 508 -> 0 bytes ...entCentersReturnsImage_center(-40,100).png | 3 + ...fferentCentersReturnsImage_center(0,0).png | 3 + ...erentCentersReturnsImage_center(0,100).png | 3 + ...erentCentersReturnsImage_center(100,0).png | 3 + ...entCentersReturnsImage_center(100,100).png | 3 + ...ushWithEqualColorsReturnsUnicolorImage.png | 3 + 14 files changed, 70 insertions(+), 73 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,100).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs deleted file mode 100644 index 3fb5cbf94..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillRadialGradientBrushTests -{ - public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color red = Color.Red; - - RadialGradientBrush unicolorRadialGradientBrush = - new( - new Point(0, 0), - 100, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorRadialGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 100)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 100)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, -40, 100)] - public void WithDifferentCentersReturnsImage( - TestImageProvider provider, - int centerX, - int centerY) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - TolerantComparer, - image => - { - RadialGradientBrush brush = new( - new Point(centerX, centerY), - image.Width / 2f, - GradientRepetitionMode.None, - new ColorStop(0, Color.Red), - new ColorStop(1, Color.Yellow)); - - image.Mutate(x => x.Fill(brush)); - }, - $"center({centerX},{centerY})", - false, - false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs index 9d04f5db5..da749723c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -14,6 +14,58 @@ public partial class ProcessWithDrawingCanvasTests { private static readonly ImageComparer EllipticGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); private static readonly ImageComparer LinearGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer RadialGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color red = Color.Red; + + RadialGradientBrush brush = + new( + new Point(0, 0), + 100, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 100)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 100)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, -40, 100)] + public void FillRadialGradientBrushWithDifferentCentersReturnsImage( + TestImageProvider provider, + int centerX, + int centerY) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + RadialGradientTolerantComparer, + image => + { + RadialGradientBrush brush = new( + new Point(centerX, centerY), + image.Width / 2F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"center({centerX},{centerY})", + false, + false); [Theory] [WithBlankImage(10, 10, PixelTypes.Rgba32)] diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png deleted file mode 100644 index 1ed0a00e74e4947c1142ca7a6a25999075c4b026..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2112 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 I?e(Pp0=6sy@&Et; diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png deleted file mode 100644 index 3a2515a1e3d64be1a4eff881f904e96969a025e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5915 zcmV+$7v$)PP)pby-7qtRCwC$osEv`Iu3*7tl$5Uoy7us zfkAF;ixMR}X#y1UduHae`ASl>(|VqA%6H@)-g~=zcVXsHzq=q1fO7)I>42gr-v5pd zkS$>7y}g5gtsr-B^xl5Q2S_*&rxK1C7$*@74IF>pe!zTygaBcI<8(lwfu%Sh`GENV zF+p(#LBdiHHXP-HpVGcc}SJAMn4`g$-L zSv^*ddLsk6Mn%p6FdrZ<1|vJDBd|n{crDiI4w#pMQeOwg42}jU2P3HOq#&9cGW*&w z0Ofd}^?C;&Z32KrO(f7}SwLY1bdQRh16Zz*93LR-RU-n9?4W-AS}}mK%3h5i`TPC{ z$Od+*iO*(@tQON|txyoHy&3^?02$;Z&3t_%9L)d+!)8SU_0i`GkV7*153RlGz5hTp zsgDAq2SS^`u-ay^f;uyxN7yUrd|f*K<^v>9HMHu{f^1gSsFywBW!tM`@UaYFd5D_q z4@d(5(ZInj4%tE7l0!1>)d4_q_q*i=ln)Ssr9wc7uvrm7Jxf85{=)(aq8zio@dLzZ zsZda?HY;k`$w~#Wst9VY5HL2kTy6_J|i%5v{$_SgO(64I$YJkTwAX z%^}e?i#h7`OtKgOonfy=ldJ37yT5b8TvE4!BN7m%%~~<)Wsi87_G+XYWdcL*bH@kB zzr(l{kgRSb!e;e8&ngL{?3IYV^RNK2NqDbY!QllYYofIxs7D2KYcc4ZTxkF~CQlZx z24r;ddjUb=Xp+;1prhUrq##;Ykj?|0T=hP8{QvQgJtkar8#tQ6YOyC;P1f`wSqkFq zKakD?omgG-MjNO|TL($q5)Kw1yV)!YD2x$rq`k6q9<#y26_|yP)Ggr124qK@6*1zC zv{&BFWAy&r$BQ7Tsc@VIIJAIdcO#6TZc&h|fX?VV5I}mWafL*)sqC?UGzA=J7V)UZ z430MFL7uE2S8xu%UI{R{x&!6!i9gC7JxC;1MREY?fx)m^)^5bJ1clLuAfPY;`UsX2 zuvh46T>;)7sYVZ!#{;4TBRaT8SS?mizkZ%s5?bxms2KEg9u{jg0_AuzAU)e+v0721 zUt}4I5N*~91wpQs7}a?ofm{Zl{JoVY0y2WZYqfT0*{wE98`eD~A*v$1z2@G@)!!4{ z8pxaCTno%3bqO3ZMezpr%x;8jvzP&$VXs)q;R)oY0+t|1;sBXVha!VJs~gF%StD4i zBVM+>vUVQKK;E-g!B&e8NHg5nYLeNFJY7LF_R89M%(4d>$Ti2YGa%Mw8|>h2-9N%! zpT!93?0{~ySF_2L29Ta=SQR5dNh%eNM!IDMch9COaX=EOAjt9(#9mqVcd!GwRz=E= zfFSD73+vI{G>vGEosv891_59(z>= z=N-XnaR8ZFqn(0#R5#+WS!huhWv_bq!vaWFAa4_BmI88j!3CCrLjmgEX`gZ+5pi!vud-*Zo!=~(Lx|$DG0+} zSvrsCKwgyD<4!$_fAs4EWOU*pHYqc>TS2kBt_X9~%L?e|W~iQ>$_nHSRFA^VBGm;4 zswC_XAGx^Eqa;k5m0_X8 zL<6~1L5e{+{z4K8h^JJ`8U?dE5;U|UHjAwwsJ+rEM?@fJjc~QfVT1$+L_<8PB;I8x z)_v}g-3S`g|9%cF2@!kc={!b*18X2J&Jk{zK;m4dRjKOH0+fWg`3ep0%!yW(f_Nua zv;7@+*~7ERsgadK`*Dc?gaJo%h}Wu#wIhkJS$7|!BVNQ_Y4QhB26_THJA`XKKG}d+ zY}Kj|ugxbOKq74x8qlrwYL-9H>|qV$>=2IFt&#-@0#9~`*NlR_-AI(pinLdZGB9$4 zYf+AXorhHohxR0Ck{jZ!xu!{cH?oU@csmbtgxeGWJR*d*N%LOtUZlF!qlwRYono_I z+T0Pb`qx??M7xc%cCGYP_(OcJ=RJa$Xk$dBxMmjAd&>j zDcq&>-rzpkKVq3^MJfo|d1%YPj1evx!aYz31CTT5W(n~bqhN1|c=tIWK!Wxk$ZB0} z_J|zevO>7`R+xZD5G>Z5!Wadkxdc%Xjm;V@HSyZ3Uj7(m5AT{PWJ$WW&#|jY?v*S%|&rDTgPJvqLz08fsAp&Ce-TizUellTkK$ zM#0vWov9!i<*){FMhMT&5FTr#vRLTb2!&w-6cOT)jwDh+G;KK=$eEYei5|;(&PN3# z)ddGjJu=HjZ-}3%iFf2{u~)M)a1_W{Aw09xW5Y18S$+kHvSnFztSN!8UQP(LSqLP@ zpzE-{P-c{MicpBib2d~#G8Kkxy)2{PW&nhtAlK0^YORW5@|0nvRw%@F3KFF-8A`$k z?wSXDq#&C7fd=xHpd7m@#MTP3Lx!=Ijn?2kDh^Qv=}oSXKps`{Jw+jukkswqSTQ!% z>>KfnfweYEt01GcTsy{{UAv04Snw$@bZpk13c|2uRxj+m+PuYT%@kxdx$=~M)`f$t z(7p59tujcY!eo?%ID~6T&Aw@v{O@)5=4)8n?nx+8c zPC?3GVueESbVZ0h_hghoB<>e1{ZBtdVOmE)j?4Q`QivVT2gCPP^$!r>mwCST4e+4L z>6^^gVu`ZPcoPI0oR2kw!lmE43q0D*T5>bB>V@#PL$Ja3R$&|~iH~|fGhn&it|9zR zId;hg?8ju~d1eBH^b5kl*vS@pVd+h=qLpB$C%VLwU-R>7+DX6r&&yJ)(c5-Wk4QW8 zz>{^w&#Uqs-|-zmfE4myv-0N^;Cn3Jt9g4k{%yqH{%hv|1jtiPR@vZtJ#WiRApgf^ z71Hn6<$ih~ly%FKKl@od>f}eDDDv+y@A2nWg8wGQeYTTYkszv}C*Tg>8)b*AP@MAb z9fEgZ^6o`(N+K*-_x0xATer0)Rb|Ej*8*jFjY>k326+$F7gd^qO9U#cHa0P^928g9pWWpj3SC~`< z@jiP*sfT7cIx@Ao>pV)3f{X$?GS$jZ5>}|sf+bWzGBN}U2(;5^K-bZmT(J}c0Yn4D zEWjTLkIzmDLs5{Dq!1FI5|$vY3FNc<(Szf>c@{%$mM6H+R3_Vcc@?HG6-10stQgC( zOTH8MnWI3iom{aM1O)`S4>QWf=&>;hiy#UTp%4tYwLsxjj;m_)%D~a&imf1sl6Xr+ zjglO}k8zg5bOFg!2nm1!?KIHVTw|?f#h_O~M0t{0m!oJ#!A0!NJj;4TD2&TutscvI zOFqj87qM2o)w&jY<+WLe)v}C&Bg@8&v9ZZw$wnE%0|g_5BVET|Fh-Lr^xC?XEx-q< zBZ&<0(bF)`6S`;=gsl)FHXn*Y;SJ$vAio3S?`9~Ey^63|jNr}=@rT&5i3BN(AQmfY zEc^H5_CUc@4s9UsbslArf@lEo1oy}ge;8GUJvLTbtktESxA^mnaJ7M4JGrvHbb-fa zS(L;&3SLz#vdTxb#qy|uBmi)y64nsz3FM>BqYc(!%|^Tej(TNsLRzhsXMaggNsa`7 z#Hl)JKr$L`Z^@^zQYWAonn12mj5>nO}?;LsbiuT zTGbeVp)CeyaGae7OB>WRejWj&C=`WZy^clJY+|o;fJ9m;6bgowvIcS)oa`QZ#ZV9d zo~?UOu!6hy0uoWMXwGRGAW?mW^+E}!_(r&zK(18|roD=?Su;Sq!Ckv@7=_^oRuc#i zA*zO@`9^m49R|ff19`6uWZ0|On3rL*kkPL3|>) z05gOiO2yEq1`^0eod?riu@!`Av#gUXWE7kk;z9MW0phh%><~W68J>yNsPkahs}uac zMZFTQ2ap~d2kFKR@kcZuvOx$(N4RJp_fD=*d&N``Z*k}agtdk%GQ>*;AS*+-CXicJ z=W06-mc5Ep5X5F#0f`Lmks&@D5HTu;HH2G6xFc&dTTWtKt?LQsnxc@cAdyPK90fCX zF-kTdj3w!ZK`}I{;VA>HlPk8p%CuRdO9nBlRxv6HYbkvyAR^o)>5Nx%(gyN>PWTZR zv(>sYYlYe?hJuJ-ms3J)7P5cD6Wj{`61tRLvH=m{tfLye#DF)DkL=VP7&GN4FH;ak zH=9B#BUY=GISO6|h>$}#JCLJ* zWZNsYf-r5C7m&!{j)ZvG0T9j0C|eaHB9PCNqKisaMyFfal+VyV#0P;FqVJaBlInlSqi z5$uYE>`CA9g^uE;Y$g5cWjN8{A9k8Ji5q z2$G00kkxr;R3j>&GlP1h&5A4$7s0UxkP#%TG7#-NR@*DfQj;BQ77LJ(dMt!v9Uy-p zITb1r-Fal#s}%~e!e)5^>FqO@UjsxgV@8dgvUVOUdnFPV&^1dA3$3y6Gx+(Uxw?}+R?GVGOismW@a71fQ%2gp3UXLKGGd$l5M>HL)s4md!%IC|jP1n*wruFj$QNJ;G3bm_9}D48#U@>+AQs+s`9wA z?SX*&eE_YmDVS-m5HMzY(j$U8+h+9udFWB)$;bch`2Q%BgIx?V>{Ygc%)p4USr0#| zyx}-Rzgic0osS566|vNWZL`qV6?uuBdM_YX+ci>+Rrbm<;*A>hvTfD_r>N$lbIn zq8RkTu~I=aV8{o?y8-Eef?h2#E1-Le!e|8%g~fXDQRVH&6%zEdJ|gTDd#MQn5{r8L zz)|H_j-r4=3&xui!~)6>9aVnqxGsWR5zu#3kPnIv5S6OPqJXa5lO7$^A`QwA(1LQH#^iEc`f_z8l@n66;^wmq!A;kay002ovPDHLkV1j?0wc7vy diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png deleted file mode 100644 index d2e3b3ebb27eafe42eb4245d0323a92a1b7b65fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72Tw7#Qbyx;TbZ+=$$1ckC@#h!1UC}l zGz=jOuX3|J_ BR=NNH diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png new file mode 100644 index 000000000..c2c164204 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:682aa4e681c9887ffd50b5bf70f2d91f0bf3cc7de880e55ff914a490faf2cc6b +size 4081 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png new file mode 100644 index 000000000..88b563c84 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4b4b963eee4836ccd4d2dee013eddf5c7974f5510786d8cba0652ff00de2860 +size 5121 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png new file mode 100644 index 000000000..8b11acddb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539aed2d3addb82ac61ebc3b4f8d52dd4f45487b63d295ffc89b71cfb8bfd4c3 +size 7371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png new file mode 100644 index 000000000..ae98e58ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cf1cb768b3e325d4554f4a0ccfcb48baf932423c7cdd80295cd0f5b65e0b042 +size 9177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png new file mode 100644 index 000000000..2cf1f58ba --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7ae5f9ece34e2d7b85f3095a82864fa203c921c01fc3b33f7d794fe5126c376 +size 13547 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..3f00c2339 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b449799e1802ccd0193bbb8b0ba69a6c589598b9ea6442fa81940c5c68ad4a7 +size 637 From c15ba00b6720f86e9f04c9e54f360537e69493d1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:43:54 +1000 Subject: [PATCH 060/136] Migrate SolidBrush tests --- .../Drawing/FillSolidBrushTests.cs | 194 ------------------ ...ssWithDrawingCanvasTests.FillSolidBrush.cs | 192 +++++++++++++++++ ...-0.5_blenderMode-Add_blendPercentage-1.png | Bin 123 -> 0 bytes ...blenderMode-Multiply_blendPercentage-1.png | Bin 123 -> 0 bytes ...5_blenderMode-Normal_blendPercentage-1.png | Bin 123 -> 0 bytes ...-1_blenderMode-Add_blendPercentage-0.5.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.5.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.5.png | Bin 123 -> 0 bytes ....5_blenderMode-Add_blendPercentage-0.3.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.3.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.3.png | Bin 123 -> 0 bytes ....8_blenderMode-Add_blendPercentage-0.8.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.8.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.8.png | Bin 123 -> 0 bytes ...-0.5_blenderMode-Add_blendPercentage-1.png | Bin 123 -> 0 bytes ...blenderMode-Multiply_blendPercentage-1.png | Bin 123 -> 0 bytes ...5_blenderMode-Normal_blendPercentage-1.png | Bin 123 -> 0 bytes ...-1_blenderMode-Add_blendPercentage-0.5.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.5.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.5.png | Bin 123 -> 0 bytes ....5_blenderMode-Add_blendPercentage-0.3.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.3.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.3.png | Bin 123 -> 0 bytes ....8_blenderMode-Add_blendPercentage-0.8.png | Bin 123 -> 0 bytes ...enderMode-Multiply_blendPercentage-0.8.png | Bin 123 -> 0 bytes ...blenderMode-Normal_blendPercentage-0.8.png | Bin 123 -> 0 bytes .../DoesNotDependOnSinglePixelType_Argb32.png | Bin 123 -> 0 bytes .../DoesNotDependOnSinglePixelType_Rgba32.png | Bin 123 -> 0 bytes ...sNotDependOnSinglePixelType_RgbaVector.png | Bin 123 -> 0 bytes .../DoesNotDependOnSize_Blank16x7.png | Bin 119 -> 0 bytes .../DoesNotDependOnSize_Blank1x1.png | Bin 107 -> 0 bytes .../DoesNotDependOnSize_Blank33x32.png | Bin 142 -> 0 bytes .../DoesNotDependOnSize_Blank400x500.png | Bin 1527 -> 0 bytes .../DoesNotDependOnSize_Blank7x4.png | Bin 116 -> 0 bytes ...Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | Bin 134 -> 0 bytes ...Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | Bin 133 -> 0 bytes ...Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | Bin 134 -> 0 bytes ...Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | Bin 133 -> 0 bytes ...lorIsOpaque_OverridePreviousColor_Blue.png | Bin 123 -> 0 bytes ...orIsOpaque_OverridePreviousColor_Khaki.png | Bin 123 -> 0 bytes ...Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | 3 + ...Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | 3 + 42 files changed, 198 insertions(+), 194 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank16x7.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs deleted file mode 100644 index 8ecbdef49..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillSolidBrushTests -{ - [Theory] - [WithBlankImage(1, 1, PixelTypes.Rgba32)] - [WithBlankImage(7, 4, PixelTypes.Rgba32)] - [WithBlankImage(16, 7, PixelTypes.Rgba32)] - [WithBlankImage(33, 32, PixelTypes.Rgba32)] - [WithBlankImage(400, 500, PixelTypes.Rgba32)] - public void DoesNotDependOnSize(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Fill(color)); - - image.DebugSave(provider, appendPixelTypeToFileName: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Fill(color)); - - image.DebugSave(provider, appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - image.Mutate(c => c.Fill(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTest(c => c.Fill(color, region), testDetails, ImageComparer.Exact); - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage( - TestImageProvider provider, - int x0, - int y0, - int w, - int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTestOnWrappedMemoryImage( - c => c.Fill(color, region), - testDetails, - ImageComparer.Exact, - useReferenceOutputFrom: nameof(this.FillRegion)); - } - - public static readonly TheoryData BlendData = - new() - { - { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - }; - - [Theory] - [WithSolidFilledImages(nameof(BlendData), 16, 16, "Red", PixelTypes.Rgba32)] - public void BlendFillColorOverBackground( - TestImageProvider provider, - bool triggerFillRegion, - string newColorName, - float alpha, - PixelColorBlendingMode blenderMode, - float blendPercentage) - where TPixel : unmanaged, IPixel - { - Color fillColor = TestUtils.GetColorByName(newColorName).WithAlpha(alpha); - - using (Image image = provider.GetImage()) - { - TPixel bgColor = image[0, 0]; - - DrawingOptions options = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - ColorBlendingMode = blenderMode, - BlendPercentage = blendPercentage - } - }; - - if (triggerFillRegion) - { - RectangularPolygon path = new(0, 0, 16, 16); - image.Mutate(c => c.SetGraphicsOptions(options.GraphicsOptions).Fill(new SolidBrush(fillColor), path)); - } - else - { - image.Mutate(c => c.Fill(options, new SolidBrush(fillColor))); - } - - var testOutputDetails = new - { - triggerFillRegion, - newColorName, - alpha, - blenderMode, - blendPercentage - }; - - image.DebugSave( - provider, - testOutputDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - PixelBlender blender = PixelOperations.Instance.GetPixelBlender( - blenderMode, - PixelAlphaCompositionMode.SrcOver); - TPixel expectedPixel = blender.Blend(bgColor, fillColor.ToPixel(), blendPercentage); - - image.ComparePixelBufferTo(expectedPixel); - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs new file mode 100644 index 000000000..77f44a164 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static readonly TheoryData FillSolidBrush_BlendData = + new() + { + { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, + { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, + { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, + }; + + [Theory] + [WithBlankImage(1, 1, PixelTypes.Rgba32)] + [WithBlankImage(7, 4, PixelTypes.Rgba32)] + [WithBlankImage(16, 7, PixelTypes.Rgba32)] + [WithBlankImage(33, 32, PixelTypes.Rgba32)] + [WithBlankImage(400, 500, PixelTypes.Rgba32)] + public void FillSolidBrush_DoesNotDependOnSize(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave(provider, appendPixelTypeToFileName: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] + public void FillSolidBrush_DoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName); + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillSolidBrush_Region(TestImageProvider provider, int x0, int y0, int w, int h) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; + Rectangle region = new(x0, y0, w, h); + Color color = Color.Blue; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(region, Brushes.Solid(color))), + testDetails, + ImageComparer.Exact); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillSolidBrush_Region_WorksOnWrappedMemoryImage( + TestImageProvider provider, + int x0, + int y0, + int w, + int h) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; + Rectangle region = new(x0, y0, w, h); + Color color = Color.Blue; + + provider.RunValidatingProcessorTestOnWrappedMemoryImage( + c => c.ProcessWithCanvas(canvas => canvas.Fill(region, Brushes.Solid(color))), + testDetails, + ImageComparer.Exact, + useReferenceOutputFrom: nameof(this.FillSolidBrush_Region)); + } + + [Theory] + [WithSolidFilledImages(nameof(FillSolidBrush_BlendData), 16, 16, "Red", PixelTypes.Rgba32)] + public void FillSolidBrush_BlendFillColorOverBackground( + TestImageProvider provider, + bool triggerFillRegion, + string newColorName, + float alpha, + PixelColorBlendingMode blenderMode, + float blendPercentage) + where TPixel : unmanaged, IPixel + { + Color fillColor = TestUtils.GetColorByName(newColorName).WithAlpha(alpha); + + using Image image = provider.GetImage(); + TPixel bgColor = image[0, 0]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + ColorBlendingMode = blenderMode, + BlendPercentage = blendPercentage + } + }; + + if (triggerFillRegion) + { + RectangularPolygon path = new(0, 0, 16, 16); + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(path, Brushes.Solid(fillColor)))); + } + else + { + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(fillColor)))); + } + + var testOutputDetails = new + { + triggerFillRegion, + newColorName, + alpha, + blenderMode, + blendPercentage + }; + + image.DebugSave( + provider, + testOutputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + blenderMode, + PixelAlphaCompositionMode.SrcOver); + TPixel expectedPixel = blender.Blend(bgColor, fillColor.ToPixel(), blendPercentage); + image.ComparePixelBufferTo(expectedPixel); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png deleted file mode 100644 index 57cb2e6daa58b0fa9e5d877c33315678c341d51e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVFVdQ&MBb@0OKzrL;wH) diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png deleted file mode 100644 index 346ea8b42bcc674f402578c629c941c6a33d8ef5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVFVdQ&MBb@0OKzrL;wH) diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png deleted file mode 100644 index e93fd5a60db8c83e3e09a6ae6fc02c54e0ebe7fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVs PK!psRu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV*P=I&jEAQNlFgN<$zsTQ?&^vo+J#+O02V Q0~IoOy85}Sb4q9e0Nht1Q~&?~ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png deleted file mode 100644 index b428583f312a116c7d1234dfaeedf79f8dbe5dad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV*P=I&jEAQNlFgN<$zsTQ?&^vo+J#+O02V Q0~IoOy85}Sb4q9e0Nht1Q~&?~ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png deleted file mode 100644 index 57cb2e6daa58b0fa9e5d877c33315678c341d51e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVFVdQ&MBb@0OKzrL;wH) diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png deleted file mode 100644 index 346ea8b42bcc674f402578c629c941c6a33d8ef5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVFVdQ&MBb@0OKzrL;wH) diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png deleted file mode 100644 index e93fd5a60db8c83e3e09a6ae6fc02c54e0ebe7fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVs PK!psRu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV*P=I&jEAQNlFgN<$zsTQ?&^vo+J#+O02V Q0~IoOy85}Sb4q9e0Nht1Q~&?~ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png deleted file mode 100644 index b428583f312a116c7d1234dfaeedf79f8dbe5dad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV*P=I&jEAQNlFgN<$zsTQ?&^vo+J#+O02V Q0~IoOy85}Sb4q9e0Nht1Q~&?~ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png deleted file mode 100644 index 1410827bc94b83cd5e917603ab2d419cb0721502..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVmdKI;Vst0GIw8j{pDw diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png deleted file mode 100644 index 4e4ee1ee16f63bedde274c4348fa889381ce74d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blmUKs7M+SzC{oH>NS%G}c0*}aI z1_r*vAk26?e?bP0l+XkKU>q4_ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png deleted file mode 100644 index 31965cc3a09475f5ebc940c2dac1aaff370aba85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^ia@Nu!3HGf><~N!q*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0XB4ude`@%$AjKtTgf7srr_TW`-9G6Hol7_@Kt_0pZEPxoNaD)qip8wEuaP?*a2 XO@Mh$(Dd%bK)no}u6{1-oD!M<@uVb5 diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png deleted file mode 100644 index f3c6b080b9e4d34794fbb71a2e5493e8ee53dc8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1527 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKNUpUx+BEq+-8-Nr`x}&cn1H;CC?mvmFKt5-I zM`SSr1K(i~W;~w1B87p0b*86_V@SoVw^tPz85lSY7_g+ibhkgza41JGZ(gu1BZIpp zBLf>#LnA|i0)qqx2+@)Uu>@uVjd^fSf$gI@4>_RNm}t!#jhIFTqNC6QS?Lgu8PrTU mw6hE_0+HfyL`Teju`ds~-iR<=cnU1J89ZJ6T-G@yGywq1xbjs1 diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png deleted file mode 100644 index 8914b9c49c58f2b0ab4f1a3ab86ee7eb383d4a82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YpyIO&eQjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pqQtNV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!poXW5V@SoVgTe~DWM4fDn%po diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f3429249693995088f7f85674b3f6068ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pt`4vV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!poXW5V@SoVgTe~DWM4fDn%po diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f3429249693995088f7f85674b3f6068ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pt`4vV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV Date: Wed, 4 Mar 2026 15:46:20 +1000 Subject: [PATCH 061/136] Migrate SweetGradientBrush tests --- .../Drawing/FillSweepGradientBrushTests.cs | 48 ------------------- ...sWithDrawingCanvasTests.GradientBrushes.cs | 32 +++++++++++++ ...llSweep_Every90Degrees_start(0,end360).png | 3 -- ...Sweep_Every90Degrees_start(180,end540).png | 3 -- ...Sweep_Every90Degrees_start(270,end630).png | 3 -- ...lSweep_Every90Degrees_start(90,end450).png | 3 -- ...llSweep_Every90Degrees_start(0,end360).png | 3 ++ ...Sweep_Every90Degrees_start(180,end540).png | 3 ++ ...Sweep_Every90Degrees_start(270,end630).png | 3 ++ ...lSweep_Every90Degrees_start(90,end450).png | 3 ++ 10 files changed, 44 insertions(+), 60 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs deleted file mode 100644 index cc4518e6d..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillSweepGradientBrushTests -{ - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0f, 360f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 90f, 450f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 180f, 540f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 270f, 630f)] - public void SweepGradientBrush_RendersFullSweep_Every90Degrees(TestImageProvider provider, float start, float end) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color green = Color.Green; - Color blue = Color.Blue; - Color yellow = Color.Yellow; - - SweepGradientBrush brush = new( - new Point(100, 100), - start, - end, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(0.25F, yellow), - new ColorStop(0.5F, green), - new ColorStop(0.75F, blue), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(brush)); - }, - $"start({start},end{end})", - false, - false); -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs index da749723c..7fbdc91f6 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -15,6 +15,38 @@ public partial class ProcessWithDrawingCanvasTests private static readonly ImageComparer EllipticGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); private static readonly ImageComparer LinearGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); private static readonly ImageComparer RadialGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer SweepGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0F, 360F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 90F, 450F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 180F, 540F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 270F, 630F)] + public void FillSweepGradientBrush_RendersFullSweep_Every90Degrees( + TestImageProvider provider, + float start, + float end) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + SweepGradientTolerantComparer, + image => + { + SweepGradientBrush brush = new( + new Point(100, 100), + start, + end, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.25F, Color.Yellow), + new ColorStop(0.5F, Color.Green), + new ColorStop(0.75F, Color.Blue), + new ColorStop(1, Color.Red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"start({start},end{end})", + false, + false); [Theory] [WithBlankImage(200, 200, PixelTypes.Rgba32)] diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png deleted file mode 100644 index 718c00be0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2f06ea31a467d5e59e0622c3834dd962f42f328cdcc8a253fcadbd38c6ea21e5 -size 10623 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png deleted file mode 100644 index 41a7c333b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:99d1013b8b1173c253532ea299ff7bbb13aee0d6bea688cb30edf989359dc2af -size 10732 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png deleted file mode 100644 index 4de5fe103..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b51e8f78f25855e033b0be07ac4568617ce4c3c6a09df03e1ca5105df35f3b53 -size 10685 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png deleted file mode 100644 index 6edeb18dd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:733a35be9abf41327757907092634424f0901b42a830daad4fb7959b50d129e2 -size 10523 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png new file mode 100644 index 000000000..0449c3a70 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aef8e625d769ce4495aed0bd9f25aa5bf82947ddb51493747d8e89b2a5d60267 +size 24210 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png new file mode 100644 index 000000000..5c94cdb6b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cdb28418145dd9089bf603c57b2c174507b424ff39453765f13159ae3618033 +size 24332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png new file mode 100644 index 000000000..abde514b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34e77c3b4254a6a59bf17ef396f43f7cdf9b54b1ef6ccc0f6f617bc69f9f73a9 +size 24423 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png new file mode 100644 index 000000000..384c6ec1b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d37683f159db8687bf1d168c749f7f5a02c65aaad7d64daf544458ff45703f4 +size 24168 From e8922e4c76c448ad2901057a3b46cf53a5327c9a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 15:49:55 +1000 Subject: [PATCH 062/136] Migrate RecolorBrush tests --- .../ProcessWithDrawingCanvasTests.Recolor.cs} | 21 +++++++++--------- ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | 3 +++ ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | 3 +++ ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | 3 +++ ...ba32_CalliphoraPartial_Yellow-Pink-0.2.png | 3 +++ ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | 3 +++ ...Rgba32_TestPattern100x100_Red-Blue-0.6.png | 3 +++ ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | Bin 342260 -> 0 bytes ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | Bin 352924 -> 0 bytes ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | Bin 3766 -> 0 bytes ...ba32_CalliphoraPartial_Yellow-Pink-0.2.png | Bin 368423 -> 0 bytes ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | Bin 5613 -> 0 bytes ...Rgba32_TestPattern100x100_Red-Blue-0.6.png | Bin 10194 -> 0 bytes 13 files changed, 28 insertions(+), 11 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/RecolorImageTests.cs => Processing/ProcessWithDrawingCanvasTests.Recolor.cs} (66%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.2.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.6.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs similarity index 66% rename from tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs index 289971c86..328154e13 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs @@ -4,17 +4,16 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing")] -public class RecolorImageTests +public partial class ProcessWithDrawingCanvasTests { [Theory] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, "Yellow", "Pink", 0.2f)] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.6f)] - public void Recolor(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) + public void RecolorImage(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) where TPixel : unmanaged, IPixel { Color sourceColor = TestUtils.GetColorByName(sourceColorName); @@ -22,13 +21,13 @@ public void Recolor(TestImageProvider provider, string sourceCol RecolorBrush brush = new(sourceColor, targetColor, threshold); FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; - provider.RunValidatingProcessorTest(x => x.Fill(brush), testInfo); + provider.RunValidatingProcessorTest(x => x.ProcessWithCanvas(canvas => canvas.Fill(brush)), testInfo); } [Theory] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] - public void Recolor_InBox(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) + public void RecolorImage_InBox(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) where TPixel : unmanaged, IPixel { Color sourceColor = TestUtils.GetColorByName(sourceColorName); @@ -37,12 +36,12 @@ public void Recolor_InBox(TestImageProvider provider, string sou FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; provider.RunValidatingProcessorTest( - x => + x => x.ProcessWithCanvas(canvas => { - Size size = x.GetCurrentSize(); - Rectangle rectangle = new(0, (size.Height / 2) - (size.Height / 4), size.Width, size.Height / 2); - x.Fill(brush, rectangle); - }, + Rectangle bounds = canvas.Bounds; + Rectangle region = new(0, (bounds.Height / 2) - (bounds.Height / 4), bounds.Width, bounds.Height / 2); + canvas.Fill(region, brush); + }), testInfo); } } diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png new file mode 100644 index 000000000..cebcbc75a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:032c48d0f1b41d73f4200ac7f702b7bb2584f5f76e8255527dd645bb606cc67d +size 361732 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png new file mode 100644 index 000000000..d108058d5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6231dc4f3124d1093131305988ae3d12606477ac6ec2a0b91c0c15b6d54b93f +size 380198 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png new file mode 100644 index 000000000..498753590 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2f7a77ed18350bcaa2daa4ad99eef1d3c9a270add4df560c0738ffeaf6ad456 +size 12300 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png new file mode 100644 index 000000000..612d67db7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d82b27a84a4707e98dcb96c6c3e62efc6c45dc6c7a87a2deb1f8f86532b1a5ec +size 388651 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png new file mode 100644 index 000000000..4bde3c324 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fb52a23ea8aba4e9a5225d801e055881550153f49ca3d14601c16540812b5c3 +size 12111 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png new file mode 100644 index 000000000..19c8c2115 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d73d10abb987d5e633ea56f50180fa38488d2cf9fe7a209ba75b357a65c141b +size 12053 diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png deleted file mode 100644 index 0d763c0f2b467b957048c615f5dedcdc938f9d14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 342260 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≶u8jN5k)F@NJv<~8T*FTE>lFsK@PTmg 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 z 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\kx;La#VMB~~R@tK=)Z}jS|_ayt};L!v4_~&)7 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 T^cyfIUOm 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

=^OW6^-54gq@( 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

im_{QeRoGua(c&-PpKEUOe~)H#36|-Ow0* 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

xo3Vzjb?RLY7n_Ig~RN7QTADZMl& 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

E>Qw^jbEm2SA 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

QP+?dl#&pQ!IvshxKRolWPu~10_XJU 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 z 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 z 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 wr Date: Wed, 4 Mar 2026 15:52:33 +1000 Subject: [PATCH 063/136] Migrate SolidBezier tests --- .../Drawing/SolidBezierTests.cs | 62 ------------------- ...rocessWithDrawingCanvasTests.Primitives.cs | 48 ++++++++++++++ ...BezierFilledBezier_Rgba32_Blank500x500.png | 3 + ...lledPolygonOpacity_Rgba32_Blank500x500.png | 3 + .../FilledBezier_Rgba32_Blank500x500.png | 3 - ...lledPolygonOpacity_Rgba32_Blank500x500.png | 3 - 6 files changed, 54 insertions(+), 68 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs deleted file mode 100644 index 11e80c5c2..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class SolidBezierTests -{ - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32)] - public void FilledBezier(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color blue = Color.Blue; - Color hotPink = Color.HotPink; - - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.BackgroundColor(blue)); - image.Mutate(x => x.Fill(hotPink, new Polygon(new CubicBezierLineSegment(simplePath)))); - image.DebugSave(provider); - image.CompareToReferenceOutput(provider); - } - } - - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32)] - public void OverlayByFilledPolygonOpacity(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color color = Color.HotPink.WithAlpha(150 / 255F); - - using (Image image = provider.GetImage() as Image) - { - image.Mutate(x => x.BackgroundColor(Color.Blue)); - image.Mutate(x => x.Fill(color, new Polygon(new CubicBezierLineSegment(simplePath)))); - image.DebugSave(provider); - image.CompareToReferenceOutput(provider); - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 7dc53d5cc..90b65c125 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -64,6 +64,54 @@ public void DrawBeziers(TestImageProvider provider, string color appendSourceFileOrDescription: false); } + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32)] + public void SolidBezierFilledBezier(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Polygon polygon = new(new CubicBezierLineSegment(simplePath)); + SolidBrush brush = Brushes.Solid(Color.HotPink); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.Blue)); + canvas.Fill(polygon, brush); + })); + } + + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32)] + public void SolidBezierOverlayByFilledPolygonOpacity(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Polygon polygon = new(new CubicBezierLineSegment(simplePath)); + SolidBrush brush = Brushes.Solid(Color.HotPink.WithAlpha(150 / 255F)); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.Blue)); + canvas.Fill(polygon, brush); + })); + } + [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png new file mode 100644 index 000000000..4406ac4f3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f440475125b34e2f937aab639972e6b030534ed45bd57e0afd9d0a55373b1a55 +size 7216 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png new file mode 100644 index 000000000..9bc8ba0f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d602f640498fa64a1968d5dd477f422dd197ceaa3696e9d6445a1579cfff824a +size 6966 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png deleted file mode 100644 index 2ec013d52..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e8d67dbbd4fc8a7f17ed6fe300033e44a050ef2044a3fb6cfd9272c6d55816f -size 3188 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png deleted file mode 100644 index 266a6d6b9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:989c843ed10a31190d812545fff20bb9fa0aeea67ca0053af31fcdb06aa6d4de -size 3004 From 64bcbb0b9089ea491c611c55ddf35e22f368ab8e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 16:06:34 +1000 Subject: [PATCH 064/136] Migrate Blending tests --- .../Drawing/SolidFillBlendedShapesTests.cs | 204 ------------------ .../ProcessWithDrawingCanvasTests.Blending.cs | 190 ++++++++++++++++ ...Ellipse_composition-Clear_blending-Add.png | 3 + ...ipse_composition-Clear_blending-Darken.png | 3 + ...e_composition-Clear_blending-HardLight.png | 3 + ...pse_composition-Clear_blending-Lighten.png | 3 + ...se_composition-Clear_blending-Multiply.png | 3 + ...ipse_composition-Clear_blending-Normal.png | 3 + ...pse_composition-Clear_blending-Overlay.png | 3 + ...ipse_composition-Clear_blending-Screen.png | 3 + ...se_composition-Clear_blending-Subtract.png | 3 + ...ipse_composition-DestAtop_blending-Add.png | 3 + ...e_composition-DestAtop_blending-Darken.png | 3 + ...omposition-DestAtop_blending-HardLight.png | 3 + ..._composition-DestAtop_blending-Lighten.png | 3 + ...composition-DestAtop_blending-Multiply.png | 3 + ...e_composition-DestAtop_blending-Normal.png | 3 + ..._composition-DestAtop_blending-Overlay.png | 3 + ...e_composition-DestAtop_blending-Screen.png | 3 + ...composition-DestAtop_blending-Subtract.png | 3 + ...llipse_composition-DestIn_blending-Add.png | 3 + ...pse_composition-DestIn_blending-Darken.png | 3 + ..._composition-DestIn_blending-HardLight.png | 3 + ...se_composition-DestIn_blending-Lighten.png | 3 + ...e_composition-DestIn_blending-Multiply.png | 3 + ...pse_composition-DestIn_blending-Normal.png | 3 + ...se_composition-DestIn_blending-Overlay.png | 3 + ...pse_composition-DestIn_blending-Screen.png | 3 + ...e_composition-DestIn_blending-Subtract.png | 3 + ...lipse_composition-DestOut_blending-Add.png | 3 + ...se_composition-DestOut_blending-Darken.png | 3 + ...composition-DestOut_blending-HardLight.png | 3 + ...e_composition-DestOut_blending-Lighten.png | 3 + ..._composition-DestOut_blending-Multiply.png | 3 + ...se_composition-DestOut_blending-Normal.png | 3 + ...e_composition-DestOut_blending-Overlay.png | 3 + ...se_composition-DestOut_blending-Screen.png | 3 + ..._composition-DestOut_blending-Subtract.png | 3 + ...ipse_composition-DestOver_blending-Add.png | 3 + ...e_composition-DestOver_blending-Darken.png | 3 + ...omposition-DestOver_blending-HardLight.png | 3 + ..._composition-DestOver_blending-Lighten.png | 3 + ...composition-DestOver_blending-Multiply.png | 3 + ...e_composition-DestOver_blending-Normal.png | 3 + ..._composition-DestOver_blending-Overlay.png | 3 + ...e_composition-DestOver_blending-Screen.png | 3 + ...composition-DestOver_blending-Subtract.png | 3 + ...kEllipse_composition-Dest_blending-Add.png | 3 + ...lipse_composition-Dest_blending-Darken.png | 3 + ...se_composition-Dest_blending-HardLight.png | 3 + ...ipse_composition-Dest_blending-Lighten.png | 3 + ...pse_composition-Dest_blending-Multiply.png | 3 + ...lipse_composition-Dest_blending-Normal.png | 3 + ...ipse_composition-Dest_blending-Overlay.png | 3 + ...lipse_composition-Dest_blending-Screen.png | 3 + ...pse_composition-Dest_blending-Subtract.png | 3 + ...lipse_composition-SrcAtop_blending-Add.png | 3 + ...se_composition-SrcAtop_blending-Darken.png | 3 + ...composition-SrcAtop_blending-HardLight.png | 3 + ...e_composition-SrcAtop_blending-Lighten.png | 3 + ..._composition-SrcAtop_blending-Multiply.png | 3 + ...se_composition-SrcAtop_blending-Normal.png | 3 + ...e_composition-SrcAtop_blending-Overlay.png | 3 + ...se_composition-SrcAtop_blending-Screen.png | 3 + ..._composition-SrcAtop_blending-Subtract.png | 3 + ...Ellipse_composition-SrcIn_blending-Add.png | 3 + ...ipse_composition-SrcIn_blending-Darken.png | 3 + ...e_composition-SrcIn_blending-HardLight.png | 3 + ...pse_composition-SrcIn_blending-Lighten.png | 3 + ...se_composition-SrcIn_blending-Multiply.png | 3 + ...ipse_composition-SrcIn_blending-Normal.png | 3 + ...pse_composition-SrcIn_blending-Overlay.png | 3 + ...ipse_composition-SrcIn_blending-Screen.png | 3 + ...se_composition-SrcIn_blending-Subtract.png | 3 + ...llipse_composition-SrcOut_blending-Add.png | 3 + ...pse_composition-SrcOut_blending-Darken.png | 3 + ..._composition-SrcOut_blending-HardLight.png | 3 + ...se_composition-SrcOut_blending-Lighten.png | 3 + ...e_composition-SrcOut_blending-Multiply.png | 3 + ...pse_composition-SrcOut_blending-Normal.png | 3 + ...se_composition-SrcOut_blending-Overlay.png | 3 + ...pse_composition-SrcOut_blending-Screen.png | 3 + ...e_composition-SrcOut_blending-Subtract.png | 3 + ...lipse_composition-SrcOver_blending-Add.png | 3 + ...se_composition-SrcOver_blending-Darken.png | 3 + ...composition-SrcOver_blending-HardLight.png | 3 + ...e_composition-SrcOver_blending-Lighten.png | 3 + ..._composition-SrcOver_blending-Multiply.png | 3 + ...se_composition-SrcOver_blending-Normal.png | 3 + ...e_composition-SrcOver_blending-Overlay.png | 3 + ...se_composition-SrcOver_blending-Screen.png | 3 + ..._composition-SrcOver_blending-Subtract.png | 3 + ...ckEllipse_composition-Src_blending-Add.png | 3 + ...llipse_composition-Src_blending-Darken.png | 3 + ...pse_composition-Src_blending-HardLight.png | 3 + ...lipse_composition-Src_blending-Lighten.png | 3 + ...ipse_composition-Src_blending-Multiply.png | 3 + ...llipse_composition-Src_blending-Normal.png | 3 + ...lipse_composition-Src_blending-Overlay.png | 3 + ...llipse_composition-Src_blending-Screen.png | 3 + ...ipse_composition-Src_blending-Subtract.png | 3 + ...ckEllipse_composition-Xor_blending-Add.png | 3 + ...llipse_composition-Xor_blending-Darken.png | 3 + ...pse_composition-Xor_blending-HardLight.png | 3 + ...lipse_composition-Xor_blending-Lighten.png | 3 + ...ipse_composition-Xor_blending-Multiply.png | 3 + ...llipse_composition-Xor_blending-Normal.png | 3 + ...lipse_composition-Xor_blending-Overlay.png | 3 + ...llipse_composition-Xor_blending-Screen.png | 3 + ...ipse_composition-Xor_blending-Subtract.png | 3 + ...Ellipse_composition-Clear_blending-Add.png | 3 + ...ipse_composition-Clear_blending-Darken.png | 3 + ...e_composition-Clear_blending-HardLight.png | 3 + ...pse_composition-Clear_blending-Lighten.png | 3 + ...se_composition-Clear_blending-Multiply.png | 3 + ...ipse_composition-Clear_blending-Normal.png | 3 + ...pse_composition-Clear_blending-Overlay.png | 3 + ...ipse_composition-Clear_blending-Screen.png | 3 + ...se_composition-Clear_blending-Subtract.png | 3 + ...pse_composition-DestAtop_blending-Add.png} | 0 ..._composition-DestAtop_blending-Darken.png} | 0 ...mposition-DestAtop_blending-HardLight.png} | 0 ...composition-DestAtop_blending-Lighten.png} | 0 ...omposition-DestAtop_blending-Multiply.png} | 0 ..._composition-DestAtop_blending-Normal.png} | 0 ...composition-DestAtop_blending-Overlay.png} | 0 ..._composition-DestAtop_blending-Screen.png} | 0 ...omposition-DestAtop_blending-Subtract.png} | 0 ...lipse_composition-DestIn_blending-Add.png} | 0 ...se_composition-DestIn_blending-Darken.png} | 0 ...composition-DestIn_blending-HardLight.png} | 0 ...e_composition-DestIn_blending-Lighten.png} | 0 ..._composition-DestIn_blending-Multiply.png} | 0 ...se_composition-DestIn_blending-Normal.png} | 0 ...e_composition-DestIn_blending-Overlay.png} | 0 ...se_composition-DestIn_blending-Screen.png} | 0 ..._composition-DestIn_blending-Subtract.png} | 0 ...lipse_composition-DestOut_blending-Add.png | 3 + ...se_composition-DestOut_blending-Darken.png | 3 + ...composition-DestOut_blending-HardLight.png | 3 + ...e_composition-DestOut_blending-Lighten.png | 3 + ..._composition-DestOut_blending-Multiply.png | 3 + ...se_composition-DestOut_blending-Normal.png | 3 + ...e_composition-DestOut_blending-Overlay.png | 3 + ...se_composition-DestOut_blending-Screen.png | 3 + ..._composition-DestOut_blending-Subtract.png | 3 + ...ipse_composition-DestOver_blending-Add.png | 3 + ...e_composition-DestOver_blending-Darken.png | 3 + ...omposition-DestOver_blending-HardLight.png | 3 + ..._composition-DestOver_blending-Lighten.png | 3 + ...composition-DestOver_blending-Multiply.png | 3 + ...e_composition-DestOver_blending-Normal.png | 3 + ..._composition-DestOver_blending-Overlay.png | 3 + ...e_composition-DestOver_blending-Screen.png | 3 + ...composition-DestOver_blending-Subtract.png | 3 + ...dEllipse_composition-Dest_blending-Add.png | 3 + ...lipse_composition-Dest_blending-Darken.png | 3 + ...se_composition-Dest_blending-HardLight.png | 3 + ...ipse_composition-Dest_blending-Lighten.png | 3 + ...pse_composition-Dest_blending-Multiply.png | 3 + ...lipse_composition-Dest_blending-Normal.png | 3 + ...ipse_composition-Dest_blending-Overlay.png | 3 + ...lipse_composition-Dest_blending-Screen.png | 3 + ...pse_composition-Dest_blending-Subtract.png | 3 + ...lipse_composition-SrcAtop_blending-Add.png | 3 + ...se_composition-SrcAtop_blending-Darken.png | 3 + ...composition-SrcAtop_blending-HardLight.png | 3 + ...e_composition-SrcAtop_blending-Lighten.png | 3 + ..._composition-SrcAtop_blending-Multiply.png | 3 + ...se_composition-SrcAtop_blending-Normal.png | 3 + ...e_composition-SrcAtop_blending-Overlay.png | 3 + ...se_composition-SrcAtop_blending-Screen.png | 3 + ..._composition-SrcAtop_blending-Subtract.png | 3 + ...llipse_composition-SrcIn_blending-Add.png} | 0 ...pse_composition-SrcIn_blending-Darken.png} | 0 ..._composition-SrcIn_blending-HardLight.png} | 0 ...se_composition-SrcIn_blending-Lighten.png} | 0 ...e_composition-SrcIn_blending-Multiply.png} | 0 ...pse_composition-SrcIn_blending-Normal.png} | 0 ...se_composition-SrcIn_blending-Overlay.png} | 0 ...pse_composition-SrcIn_blending-Screen.png} | 0 ...e_composition-SrcIn_blending-Subtract.png} | 0 ...lipse_composition-SrcOut_blending-Add.png} | 0 ...se_composition-SrcOut_blending-Darken.png} | 0 ...composition-SrcOut_blending-HardLight.png} | 0 ...e_composition-SrcOut_blending-Lighten.png} | 0 ..._composition-SrcOut_blending-Multiply.png} | 0 ...se_composition-SrcOut_blending-Normal.png} | 0 ...e_composition-SrcOut_blending-Overlay.png} | 0 ...se_composition-SrcOut_blending-Screen.png} | 0 ..._composition-SrcOut_blending-Subtract.png} | 0 ...lipse_composition-SrcOver_blending-Add.png | 3 + ...se_composition-SrcOver_blending-Darken.png | 3 + ...composition-SrcOver_blending-HardLight.png | 3 + ...e_composition-SrcOver_blending-Lighten.png | 3 + ..._composition-SrcOver_blending-Multiply.png | 3 + ...se_composition-SrcOver_blending-Normal.png | 3 + ...e_composition-SrcOver_blending-Overlay.png | 3 + ...se_composition-SrcOver_blending-Screen.png | 3 + ..._composition-SrcOver_blending-Subtract.png | 3 + ...dEllipse_composition-Src_blending-Add.png} | 0 ...lipse_composition-Src_blending-Darken.png} | 0 ...se_composition-Src_blending-HardLight.png} | 0 ...ipse_composition-Src_blending-Lighten.png} | 0 ...pse_composition-Src_blending-Multiply.png} | 0 ...lipse_composition-Src_blending-Normal.png} | 0 ...ipse_composition-Src_blending-Overlay.png} | 0 ...lipse_composition-Src_blending-Screen.png} | 0 ...pse_composition-Src_blending-Subtract.png} | 0 ...edEllipse_composition-Xor_blending-Add.png | 3 + ...llipse_composition-Xor_blending-Darken.png | 3 + ...pse_composition-Xor_blending-HardLight.png | 3 + ...lipse_composition-Xor_blending-Lighten.png | 3 + ...ipse_composition-Xor_blending-Multiply.png | 3 + ...llipse_composition-Xor_blending-Normal.png | 3 + ...lipse_composition-Xor_blending-Overlay.png | 3 + ...llipse_composition-Xor_blending-Screen.png | 3 + ...ipse_composition-Xor_blending-Subtract.png | 3 + ...Ellipse_composition-Clear_blending-Add.png | 3 + ...ipse_composition-Clear_blending-Darken.png | 3 + ...e_composition-Clear_blending-HardLight.png | 3 + ...pse_composition-Clear_blending-Lighten.png | 3 + ...se_composition-Clear_blending-Multiply.png | 3 + ...ipse_composition-Clear_blending-Normal.png | 3 + ...pse_composition-Clear_blending-Overlay.png | 3 + ...ipse_composition-Clear_blending-Screen.png | 3 + ...se_composition-Clear_blending-Subtract.png | 3 + ...ipse_composition-DestAtop_blending-Add.png | 3 + ...e_composition-DestAtop_blending-Darken.png | 3 + ...omposition-DestAtop_blending-HardLight.png | 3 + ..._composition-DestAtop_blending-Lighten.png | 3 + ...composition-DestAtop_blending-Multiply.png | 3 + ...e_composition-DestAtop_blending-Normal.png | 3 + ..._composition-DestAtop_blending-Overlay.png | 3 + ...e_composition-DestAtop_blending-Screen.png | 3 + ...composition-DestAtop_blending-Subtract.png | 3 + ...llipse_composition-DestIn_blending-Add.png | 3 + ...pse_composition-DestIn_blending-Darken.png | 3 + ..._composition-DestIn_blending-HardLight.png | 3 + ...se_composition-DestIn_blending-Lighten.png | 3 + ...e_composition-DestIn_blending-Multiply.png | 3 + ...pse_composition-DestIn_blending-Normal.png | 3 + ...se_composition-DestIn_blending-Overlay.png | 3 + ...pse_composition-DestIn_blending-Screen.png | 3 + ...e_composition-DestIn_blending-Subtract.png | 3 + ...lipse_composition-DestOut_blending-Add.png | 3 + ...se_composition-DestOut_blending-Darken.png | 3 + ...composition-DestOut_blending-HardLight.png | 3 + ...e_composition-DestOut_blending-Lighten.png | 3 + ..._composition-DestOut_blending-Multiply.png | 3 + ...se_composition-DestOut_blending-Normal.png | 3 + ...e_composition-DestOut_blending-Overlay.png | 3 + ...se_composition-DestOut_blending-Screen.png | 3 + ..._composition-DestOut_blending-Subtract.png | 3 + ...ipse_composition-DestOver_blending-Add.png | 3 + ...e_composition-DestOver_blending-Darken.png | 3 + ...omposition-DestOver_blending-HardLight.png | 3 + ..._composition-DestOver_blending-Lighten.png | 3 + ...composition-DestOver_blending-Multiply.png | 3 + ...e_composition-DestOver_blending-Normal.png | 3 + ..._composition-DestOver_blending-Overlay.png | 3 + ...e_composition-DestOver_blending-Screen.png | 3 + ...composition-DestOver_blending-Subtract.png | 3 + ...tEllipse_composition-Dest_blending-Add.png | 3 + ...lipse_composition-Dest_blending-Darken.png | 3 + ...se_composition-Dest_blending-HardLight.png | 3 + ...ipse_composition-Dest_blending-Lighten.png | 3 + ...pse_composition-Dest_blending-Multiply.png | 3 + ...lipse_composition-Dest_blending-Normal.png | 3 + ...ipse_composition-Dest_blending-Overlay.png | 3 + ...lipse_composition-Dest_blending-Screen.png | 3 + ...pse_composition-Dest_blending-Subtract.png | 3 + ...lipse_composition-SrcAtop_blending-Add.png | 3 + ...se_composition-SrcAtop_blending-Darken.png | 3 + ...composition-SrcAtop_blending-HardLight.png | 3 + ...e_composition-SrcAtop_blending-Lighten.png | 3 + ..._composition-SrcAtop_blending-Multiply.png | 3 + ...se_composition-SrcAtop_blending-Normal.png | 3 + ...e_composition-SrcAtop_blending-Overlay.png | 3 + ...se_composition-SrcAtop_blending-Screen.png | 3 + ..._composition-SrcAtop_blending-Subtract.png | 3 + ...Ellipse_composition-SrcIn_blending-Add.png | 3 + ...ipse_composition-SrcIn_blending-Darken.png | 3 + ...e_composition-SrcIn_blending-HardLight.png | 3 + ...pse_composition-SrcIn_blending-Lighten.png | 3 + ...se_composition-SrcIn_blending-Multiply.png | 3 + ...ipse_composition-SrcIn_blending-Normal.png | 3 + ...pse_composition-SrcIn_blending-Overlay.png | 3 + ...ipse_composition-SrcIn_blending-Screen.png | 3 + ...se_composition-SrcIn_blending-Subtract.png | 3 + ...llipse_composition-SrcOut_blending-Add.png | 3 + ...pse_composition-SrcOut_blending-Darken.png | 3 + ..._composition-SrcOut_blending-HardLight.png | 3 + ...se_composition-SrcOut_blending-Lighten.png | 3 + ...e_composition-SrcOut_blending-Multiply.png | 3 + ...pse_composition-SrcOut_blending-Normal.png | 3 + ...se_composition-SrcOut_blending-Overlay.png | 3 + ...pse_composition-SrcOut_blending-Screen.png | 3 + ...e_composition-SrcOut_blending-Subtract.png | 3 + ...lipse_composition-SrcOver_blending-Add.png | 3 + ...se_composition-SrcOver_blending-Darken.png | 3 + ...composition-SrcOver_blending-HardLight.png | 3 + ...e_composition-SrcOver_blending-Lighten.png | 3 + ..._composition-SrcOver_blending-Multiply.png | 3 + ...se_composition-SrcOver_blending-Normal.png | 3 + ...e_composition-SrcOver_blending-Overlay.png | 3 + ...se_composition-SrcOver_blending-Screen.png | 3 + ..._composition-SrcOver_blending-Subtract.png | 3 + ...ntEllipse_composition-Src_blending-Add.png | 3 + ...llipse_composition-Src_blending-Darken.png | 3 + ...pse_composition-Src_blending-HardLight.png | 3 + ...lipse_composition-Src_blending-Lighten.png | 3 + ...ipse_composition-Src_blending-Multiply.png | 3 + ...llipse_composition-Src_blending-Normal.png | 3 + ...lipse_composition-Src_blending-Overlay.png | 3 + ...llipse_composition-Src_blending-Screen.png | 3 + ...ipse_composition-Src_blending-Subtract.png | 3 + ...ntEllipse_composition-Xor_blending-Add.png | 3 + ...llipse_composition-Xor_blending-Darken.png | 3 + ...pse_composition-Xor_blending-HardLight.png | 3 + ...lipse_composition-Xor_blending-Lighten.png | 3 + ...ipse_composition-Xor_blending-Multiply.png | 3 + ...llipse_composition-Xor_blending-Normal.png | 3 + ...lipse_composition-Xor_blending-Overlay.png | 3 + ...llipse_composition-Xor_blending-Screen.png | 3 + ...ipse_composition-Xor_blending-Subtract.png | 3 + ...inkRect_composition-Clear_blending-Add.png | 3 + ...Rect_composition-Clear_blending-Darken.png | 3 + ...t_composition-Clear_blending-HardLight.png | 3 + ...ect_composition-Clear_blending-Lighten.png | 3 + ...ct_composition-Clear_blending-Multiply.png | 3 + ...Rect_composition-Clear_blending-Normal.png | 3 + ...ect_composition-Clear_blending-Overlay.png | 3 + ...Rect_composition-Clear_blending-Screen.png | 3 + ...ct_composition-Clear_blending-Subtract.png | 3 + ...Rect_composition-DestAtop_blending-Add.png | 3 + ...t_composition-DestAtop_blending-Darken.png | 3 + ...omposition-DestAtop_blending-HardLight.png | 3 + ..._composition-DestAtop_blending-Lighten.png | 3 + ...composition-DestAtop_blending-Multiply.png | 3 + ...t_composition-DestAtop_blending-Normal.png | 3 + ..._composition-DestAtop_blending-Overlay.png | 3 + ...t_composition-DestAtop_blending-Screen.png | 3 + ...composition-DestAtop_blending-Subtract.png | 3 + ...nkRect_composition-DestIn_blending-Add.png | 3 + ...ect_composition-DestIn_blending-Darken.png | 3 + ..._composition-DestIn_blending-HardLight.png | 3 + ...ct_composition-DestIn_blending-Lighten.png | 3 + ...t_composition-DestIn_blending-Multiply.png | 3 + ...ect_composition-DestIn_blending-Normal.png | 3 + ...ct_composition-DestIn_blending-Overlay.png | 3 + ...ect_composition-DestIn_blending-Screen.png | 3 + ...t_composition-DestIn_blending-Subtract.png | 3 + ...kRect_composition-DestOut_blending-Add.png | 3 + ...ct_composition-DestOut_blending-Darken.png | 3 + ...composition-DestOut_blending-HardLight.png | 3 + ...t_composition-DestOut_blending-Lighten.png | 3 + ..._composition-DestOut_blending-Multiply.png | 3 + ...ct_composition-DestOut_blending-Normal.png | 3 + ...t_composition-DestOut_blending-Overlay.png | 3 + ...ct_composition-DestOut_blending-Screen.png | 3 + ..._composition-DestOut_blending-Subtract.png | 3 + ...Rect_composition-DestOver_blending-Add.png | 3 + ...t_composition-DestOver_blending-Darken.png | 3 + ...omposition-DestOver_blending-HardLight.png | 3 + ..._composition-DestOver_blending-Lighten.png | 3 + ...composition-DestOver_blending-Multiply.png | 3 + ...t_composition-DestOver_blending-Normal.png | 3 + ..._composition-DestOver_blending-Overlay.png | 3 + ...t_composition-DestOver_blending-Screen.png | 3 + ...composition-DestOver_blending-Subtract.png | 3 + ...PinkRect_composition-Dest_blending-Add.png | 3 + ...kRect_composition-Dest_blending-Darken.png | 3 + ...ct_composition-Dest_blending-HardLight.png | 3 + ...Rect_composition-Dest_blending-Lighten.png | 3 + ...ect_composition-Dest_blending-Multiply.png | 3 + ...kRect_composition-Dest_blending-Normal.png | 3 + ...Rect_composition-Dest_blending-Overlay.png | 3 + ...kRect_composition-Dest_blending-Screen.png | 3 + ...ect_composition-Dest_blending-Subtract.png | 3 + ...kRect_composition-SrcAtop_blending-Add.png | 3 + ...ct_composition-SrcAtop_blending-Darken.png | 3 + ...composition-SrcAtop_blending-HardLight.png | 3 + ...t_composition-SrcAtop_blending-Lighten.png | 3 + ..._composition-SrcAtop_blending-Multiply.png | 3 + ...ct_composition-SrcAtop_blending-Normal.png | 3 + ...t_composition-SrcAtop_blending-Overlay.png | 3 + ...ct_composition-SrcAtop_blending-Screen.png | 3 + ..._composition-SrcAtop_blending-Subtract.png | 3 + ...inkRect_composition-SrcIn_blending-Add.png | 3 + ...Rect_composition-SrcIn_blending-Darken.png | 3 + ...t_composition-SrcIn_blending-HardLight.png | 3 + ...ect_composition-SrcIn_blending-Lighten.png | 3 + ...ct_composition-SrcIn_blending-Multiply.png | 3 + ...Rect_composition-SrcIn_blending-Normal.png | 3 + ...ect_composition-SrcIn_blending-Overlay.png | 3 + ...Rect_composition-SrcIn_blending-Screen.png | 3 + ...ct_composition-SrcIn_blending-Subtract.png | 3 + ...nkRect_composition-SrcOut_blending-Add.png | 3 + ...ect_composition-SrcOut_blending-Darken.png | 3 + ..._composition-SrcOut_blending-HardLight.png | 3 + ...ct_composition-SrcOut_blending-Lighten.png | 3 + ...t_composition-SrcOut_blending-Multiply.png | 3 + ...ect_composition-SrcOut_blending-Normal.png | 3 + ...ct_composition-SrcOut_blending-Overlay.png | 3 + ...ect_composition-SrcOut_blending-Screen.png | 3 + ...t_composition-SrcOut_blending-Subtract.png | 3 + ...kRect_composition-SrcOver_blending-Add.png | 3 + ...ct_composition-SrcOver_blending-Darken.png | 3 + ...composition-SrcOver_blending-HardLight.png | 3 + ...t_composition-SrcOver_blending-Lighten.png | 3 + ..._composition-SrcOver_blending-Multiply.png | 3 + ...ct_composition-SrcOver_blending-Normal.png | 3 + ...t_composition-SrcOver_blending-Overlay.png | 3 + ...ct_composition-SrcOver_blending-Screen.png | 3 + ..._composition-SrcOver_blending-Subtract.png | 3 + ...tPinkRect_composition-Src_blending-Add.png | 3 + ...nkRect_composition-Src_blending-Darken.png | 3 + ...ect_composition-Src_blending-HardLight.png | 3 + ...kRect_composition-Src_blending-Lighten.png | 3 + ...Rect_composition-Src_blending-Multiply.png | 3 + ...nkRect_composition-Src_blending-Normal.png | 3 + ...kRect_composition-Src_blending-Overlay.png | 3 + ...nkRect_composition-Src_blending-Screen.png | 3 + ...Rect_composition-Src_blending-Subtract.png | 3 + ...tPinkRect_composition-Xor_blending-Add.png | 3 + ...nkRect_composition-Xor_blending-Darken.png | 3 + ...ect_composition-Xor_blending-HardLight.png | 3 + ...kRect_composition-Xor_blending-Lighten.png | 3 + ...Rect_composition-Xor_blending-Multiply.png | 3 + ...nkRect_composition-Xor_blending-Normal.png | 3 + ...kRect_composition-Xor_blending-Overlay.png | 3 + ...nkRect_composition-Xor_blending-Screen.png | 3 + ...Rect_composition-Xor_blending-Subtract.png | 3 + ...Ellipse_composition-Clear_blending-Add.png | Bin 372 -> 0 bytes ...ipse_composition-Clear_blending-Darken.png | Bin 372 -> 0 bytes ...e_composition-Clear_blending-HardLight.png | Bin 372 -> 0 bytes ...pse_composition-Clear_blending-Lighten.png | Bin 372 -> 0 bytes ...se_composition-Clear_blending-Multiply.png | Bin 372 -> 0 bytes ...ipse_composition-Clear_blending-Normal.png | Bin 372 -> 0 bytes ...pse_composition-Clear_blending-Overlay.png | Bin 372 -> 0 bytes ...ipse_composition-Clear_blending-Screen.png | Bin 372 -> 0 bytes ...se_composition-Clear_blending-Subtract.png | Bin 372 -> 0 bytes ...ipse_composition-DestAtop_blending-Add.png | Bin 1674 -> 0 bytes ...e_composition-DestAtop_blending-Darken.png | Bin 1635 -> 0 bytes ...omposition-DestAtop_blending-HardLight.png | Bin 1673 -> 0 bytes ..._composition-DestAtop_blending-Lighten.png | Bin 1674 -> 0 bytes ...composition-DestAtop_blending-Multiply.png | Bin 1635 -> 0 bytes ...e_composition-DestAtop_blending-Normal.png | Bin 1674 -> 0 bytes ..._composition-DestAtop_blending-Overlay.png | Bin 1635 -> 0 bytes ...e_composition-DestAtop_blending-Screen.png | Bin 1674 -> 0 bytes ...composition-DestAtop_blending-Subtract.png | Bin 1635 -> 0 bytes ...llipse_composition-DestIn_blending-Add.png | Bin 734 -> 0 bytes ...pse_composition-DestIn_blending-Darken.png | Bin 734 -> 0 bytes ..._composition-DestIn_blending-HardLight.png | Bin 734 -> 0 bytes ...se_composition-DestIn_blending-Lighten.png | Bin 734 -> 0 bytes ...e_composition-DestIn_blending-Multiply.png | Bin 734 -> 0 bytes ...pse_composition-DestIn_blending-Normal.png | Bin 734 -> 0 bytes ...se_composition-DestIn_blending-Overlay.png | Bin 734 -> 0 bytes ...pse_composition-DestIn_blending-Screen.png | Bin 734 -> 0 bytes ...e_composition-DestIn_blending-Subtract.png | Bin 734 -> 0 bytes ...lipse_composition-DestOut_blending-Add.png | Bin 939 -> 0 bytes ...se_composition-DestOut_blending-Darken.png | Bin 939 -> 0 bytes ...composition-DestOut_blending-HardLight.png | Bin 939 -> 0 bytes ...e_composition-DestOut_blending-Lighten.png | Bin 939 -> 0 bytes ..._composition-DestOut_blending-Multiply.png | Bin 939 -> 0 bytes ...se_composition-DestOut_blending-Normal.png | Bin 939 -> 0 bytes ...e_composition-DestOut_blending-Overlay.png | Bin 939 -> 0 bytes ...se_composition-DestOut_blending-Screen.png | Bin 939 -> 0 bytes ..._composition-DestOut_blending-Subtract.png | Bin 939 -> 0 bytes ...ipse_composition-DestOver_blending-Add.png | Bin 1461 -> 0 bytes ...e_composition-DestOver_blending-Darken.png | Bin 1936 -> 0 bytes ...omposition-DestOver_blending-HardLight.png | Bin 1945 -> 0 bytes ..._composition-DestOver_blending-Lighten.png | Bin 1461 -> 0 bytes ...composition-DestOver_blending-Multiply.png | Bin 1936 -> 0 bytes ...e_composition-DestOver_blending-Normal.png | Bin 1461 -> 0 bytes ..._composition-DestOver_blending-Overlay.png | Bin 1936 -> 0 bytes ...e_composition-DestOver_blending-Screen.png | Bin 1461 -> 0 bytes ...composition-DestOver_blending-Subtract.png | Bin 1936 -> 0 bytes ...kEllipse_composition-Dest_blending-Add.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Darken.png | Bin 524 -> 0 bytes ...se_composition-Dest_blending-HardLight.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Lighten.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Multiply.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Normal.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Overlay.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Screen.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Subtract.png | Bin 524 -> 0 bytes ...lipse_composition-SrcAtop_blending-Add.png | Bin 524 -> 0 bytes ...se_composition-SrcAtop_blending-Darken.png | Bin 959 -> 0 bytes ...composition-SrcAtop_blending-HardLight.png | Bin 959 -> 0 bytes ...e_composition-SrcAtop_blending-Lighten.png | Bin 524 -> 0 bytes ..._composition-SrcAtop_blending-Multiply.png | Bin 959 -> 0 bytes ...se_composition-SrcAtop_blending-Normal.png | Bin 959 -> 0 bytes ...e_composition-SrcAtop_blending-Overlay.png | Bin 954 -> 0 bytes ...se_composition-SrcAtop_blending-Screen.png | Bin 524 -> 0 bytes ..._composition-SrcAtop_blending-Subtract.png | Bin 524 -> 0 bytes ...Ellipse_composition-SrcIn_blending-Add.png | Bin 739 -> 0 bytes ...ipse_composition-SrcIn_blending-Darken.png | Bin 739 -> 0 bytes ...e_composition-SrcIn_blending-HardLight.png | Bin 739 -> 0 bytes ...pse_composition-SrcIn_blending-Lighten.png | Bin 739 -> 0 bytes ...se_composition-SrcIn_blending-Multiply.png | Bin 739 -> 0 bytes ...ipse_composition-SrcIn_blending-Normal.png | Bin 739 -> 0 bytes ...pse_composition-SrcIn_blending-Overlay.png | Bin 739 -> 0 bytes ...ipse_composition-SrcIn_blending-Screen.png | Bin 739 -> 0 bytes ...se_composition-SrcIn_blending-Subtract.png | Bin 739 -> 0 bytes ...llipse_composition-SrcOut_blending-Add.png | Bin 1270 -> 0 bytes ...pse_composition-SrcOut_blending-Darken.png | Bin 1270 -> 0 bytes ..._composition-SrcOut_blending-HardLight.png | Bin 1270 -> 0 bytes ...se_composition-SrcOut_blending-Lighten.png | Bin 1270 -> 0 bytes ...e_composition-SrcOut_blending-Multiply.png | Bin 1270 -> 0 bytes ...pse_composition-SrcOut_blending-Normal.png | Bin 1270 -> 0 bytes ...se_composition-SrcOut_blending-Overlay.png | Bin 1270 -> 0 bytes ...pse_composition-SrcOut_blending-Screen.png | Bin 1270 -> 0 bytes ...e_composition-SrcOut_blending-Subtract.png | Bin 1270 -> 0 bytes ...lipse_composition-SrcOver_blending-Add.png | Bin 1461 -> 0 bytes ...se_composition-SrcOver_blending-Darken.png | Bin 1936 -> 0 bytes ...composition-SrcOver_blending-HardLight.png | Bin 1936 -> 0 bytes ...e_composition-SrcOver_blending-Lighten.png | Bin 1461 -> 0 bytes ..._composition-SrcOver_blending-Multiply.png | Bin 1936 -> 0 bytes ...se_composition-SrcOver_blending-Normal.png | Bin 1936 -> 0 bytes ...e_composition-SrcOver_blending-Overlay.png | Bin 1945 -> 0 bytes ...se_composition-SrcOver_blending-Screen.png | Bin 1461 -> 0 bytes ..._composition-SrcOver_blending-Subtract.png | Bin 1461 -> 0 bytes ...ckEllipse_composition-Src_blending-Add.png | Bin 1635 -> 0 bytes ...llipse_composition-Src_blending-Darken.png | Bin 1635 -> 0 bytes ...pse_composition-Src_blending-HardLight.png | Bin 1635 -> 0 bytes ...lipse_composition-Src_blending-Lighten.png | Bin 1635 -> 0 bytes ...ipse_composition-Src_blending-Multiply.png | Bin 1635 -> 0 bytes ...llipse_composition-Src_blending-Normal.png | Bin 1635 -> 0 bytes ...lipse_composition-Src_blending-Overlay.png | Bin 1635 -> 0 bytes ...llipse_composition-Src_blending-Screen.png | Bin 1635 -> 0 bytes ...ipse_composition-Src_blending-Subtract.png | Bin 1635 -> 0 bytes ...ckEllipse_composition-Xor_blending-Add.png | Bin 1874 -> 0 bytes ...llipse_composition-Xor_blending-Darken.png | Bin 1874 -> 0 bytes ...pse_composition-Xor_blending-HardLight.png | Bin 1874 -> 0 bytes ...lipse_composition-Xor_blending-Lighten.png | Bin 1874 -> 0 bytes ...ipse_composition-Xor_blending-Multiply.png | Bin 1874 -> 0 bytes ...llipse_composition-Xor_blending-Normal.png | Bin 1874 -> 0 bytes ...lipse_composition-Xor_blending-Overlay.png | Bin 1874 -> 0 bytes ...llipse_composition-Xor_blending-Screen.png | Bin 1874 -> 0 bytes ...ipse_composition-Xor_blending-Subtract.png | Bin 1874 -> 0 bytes ...Ellipse_composition-Clear_blending-Add.png | 3 - ...ipse_composition-Clear_blending-Darken.png | 3 - ...e_composition-Clear_blending-HardLight.png | 3 - ...pse_composition-Clear_blending-Lighten.png | 3 - ...se_composition-Clear_blending-Multiply.png | 3 - ...ipse_composition-Clear_blending-Normal.png | 3 - ...pse_composition-Clear_blending-Overlay.png | 3 - ...ipse_composition-Clear_blending-Screen.png | 3 - ...se_composition-Clear_blending-Subtract.png | 3 - ...lipse_composition-DestOut_blending-Add.png | Bin 959 -> 0 bytes ...se_composition-DestOut_blending-Darken.png | Bin 959 -> 0 bytes ...composition-DestOut_blending-HardLight.png | Bin 959 -> 0 bytes ...e_composition-DestOut_blending-Lighten.png | Bin 959 -> 0 bytes ..._composition-DestOut_blending-Multiply.png | Bin 959 -> 0 bytes ...se_composition-DestOut_blending-Normal.png | Bin 959 -> 0 bytes ...e_composition-DestOut_blending-Overlay.png | Bin 959 -> 0 bytes ...se_composition-DestOut_blending-Screen.png | Bin 959 -> 0 bytes ..._composition-DestOut_blending-Subtract.png | Bin 959 -> 0 bytes ...ipse_composition-DestOver_blending-Add.png | Bin 1560 -> 0 bytes ...e_composition-DestOver_blending-Darken.png | Bin 2229 -> 0 bytes ...omposition-DestOver_blending-HardLight.png | Bin 2158 -> 0 bytes ..._composition-DestOver_blending-Lighten.png | Bin 1553 -> 0 bytes ...composition-DestOver_blending-Multiply.png | Bin 2226 -> 0 bytes ...e_composition-DestOver_blending-Normal.png | Bin 1290 -> 0 bytes ..._composition-DestOver_blending-Overlay.png | Bin 2361 -> 0 bytes ...e_composition-DestOver_blending-Screen.png | Bin 1559 -> 0 bytes ...composition-DestOver_blending-Subtract.png | Bin 2594 -> 0 bytes ...dEllipse_composition-Dest_blending-Add.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Darken.png | Bin 524 -> 0 bytes ...se_composition-Dest_blending-HardLight.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Lighten.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Multiply.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Normal.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Overlay.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Screen.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Subtract.png | Bin 524 -> 0 bytes ...lipse_composition-SrcAtop_blending-Add.png | Bin 950 -> 0 bytes ...se_composition-SrcAtop_blending-Darken.png | Bin 1045 -> 0 bytes ...composition-SrcAtop_blending-HardLight.png | Bin 1363 -> 0 bytes ...e_composition-SrcAtop_blending-Lighten.png | Bin 953 -> 0 bytes ..._composition-SrcAtop_blending-Multiply.png | Bin 1203 -> 0 bytes ...se_composition-SrcAtop_blending-Normal.png | Bin 1432 -> 0 bytes ...e_composition-SrcAtop_blending-Overlay.png | Bin 1167 -> 0 bytes ...se_composition-SrcAtop_blending-Screen.png | Bin 952 -> 0 bytes ..._composition-SrcAtop_blending-Subtract.png | Bin 668 -> 0 bytes ...lipse_composition-SrcOver_blending-Add.png | Bin 1560 -> 0 bytes ...se_composition-SrcOver_blending-Darken.png | Bin 2229 -> 0 bytes ...composition-SrcOver_blending-HardLight.png | Bin 2361 -> 0 bytes ...e_composition-SrcOver_blending-Lighten.png | Bin 1553 -> 0 bytes ..._composition-SrcOver_blending-Multiply.png | Bin 2226 -> 0 bytes ...se_composition-SrcOver_blending-Normal.png | Bin 2426 -> 0 bytes ...e_composition-SrcOver_blending-Overlay.png | Bin 2158 -> 0 bytes ...se_composition-SrcOver_blending-Screen.png | Bin 1559 -> 0 bytes ..._composition-SrcOver_blending-Subtract.png | Bin 1630 -> 0 bytes ...edEllipse_composition-Xor_blending-Add.png | Bin 2182 -> 0 bytes ...llipse_composition-Xor_blending-Darken.png | Bin 2182 -> 0 bytes ...pse_composition-Xor_blending-HardLight.png | Bin 2182 -> 0 bytes ...lipse_composition-Xor_blending-Lighten.png | Bin 2182 -> 0 bytes ...ipse_composition-Xor_blending-Multiply.png | Bin 2182 -> 0 bytes ...llipse_composition-Xor_blending-Normal.png | Bin 2182 -> 0 bytes ...lipse_composition-Xor_blending-Overlay.png | Bin 2182 -> 0 bytes ...llipse_composition-Xor_blending-Screen.png | Bin 2182 -> 0 bytes ...ipse_composition-Xor_blending-Subtract.png | Bin 2182 -> 0 bytes ...Ellipse_composition-Clear_blending-Add.png | 3 - ...ipse_composition-Clear_blending-Darken.png | 3 - ...e_composition-Clear_blending-HardLight.png | 3 - ...pse_composition-Clear_blending-Lighten.png | 3 - ...se_composition-Clear_blending-Multiply.png | 3 - ...ipse_composition-Clear_blending-Normal.png | 3 - ...pse_composition-Clear_blending-Overlay.png | 3 - ...ipse_composition-Clear_blending-Screen.png | 3 - ...se_composition-Clear_blending-Subtract.png | 3 - ...ipse_composition-DestAtop_blending-Add.png | 3 - ...e_composition-DestAtop_blending-Darken.png | 3 - ...omposition-DestAtop_blending-HardLight.png | 3 - ..._composition-DestAtop_blending-Lighten.png | 3 - ...composition-DestAtop_blending-Multiply.png | 3 - ...e_composition-DestAtop_blending-Normal.png | 3 - ..._composition-DestAtop_blending-Overlay.png | 3 - ...e_composition-DestAtop_blending-Screen.png | 3 - ...composition-DestAtop_blending-Subtract.png | 3 - ...llipse_composition-DestIn_blending-Add.png | 3 - ...pse_composition-DestIn_blending-Darken.png | 3 - ..._composition-DestIn_blending-HardLight.png | 3 - ...se_composition-DestIn_blending-Lighten.png | 3 - ...e_composition-DestIn_blending-Multiply.png | 3 - ...pse_composition-DestIn_blending-Normal.png | 3 - ...se_composition-DestIn_blending-Overlay.png | 3 - ...pse_composition-DestIn_blending-Screen.png | 3 - ...e_composition-DestIn_blending-Subtract.png | 3 - ...lipse_composition-DestOut_blending-Add.png | Bin 670 -> 0 bytes ...se_composition-DestOut_blending-Darken.png | Bin 670 -> 0 bytes ...composition-DestOut_blending-HardLight.png | Bin 670 -> 0 bytes ...e_composition-DestOut_blending-Lighten.png | Bin 670 -> 0 bytes ..._composition-DestOut_blending-Multiply.png | Bin 670 -> 0 bytes ...se_composition-DestOut_blending-Normal.png | Bin 670 -> 0 bytes ...e_composition-DestOut_blending-Overlay.png | Bin 670 -> 0 bytes ...se_composition-DestOut_blending-Screen.png | Bin 670 -> 0 bytes ..._composition-DestOut_blending-Subtract.png | Bin 670 -> 0 bytes ...ipse_composition-DestOver_blending-Add.png | Bin 697 -> 0 bytes ...e_composition-DestOver_blending-Darken.png | Bin 690 -> 0 bytes ...omposition-DestOver_blending-HardLight.png | Bin 699 -> 0 bytes ..._composition-DestOver_blending-Lighten.png | Bin 691 -> 0 bytes ...composition-DestOver_blending-Multiply.png | Bin 699 -> 0 bytes ...e_composition-DestOver_blending-Normal.png | Bin 690 -> 0 bytes ..._composition-DestOver_blending-Overlay.png | Bin 700 -> 0 bytes ...e_composition-DestOver_blending-Screen.png | Bin 696 -> 0 bytes ...composition-DestOver_blending-Subtract.png | Bin 695 -> 0 bytes ...tEllipse_composition-Dest_blending-Add.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Darken.png | Bin 524 -> 0 bytes ...se_composition-Dest_blending-HardLight.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Lighten.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Multiply.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Normal.png | Bin 524 -> 0 bytes ...ipse_composition-Dest_blending-Overlay.png | Bin 524 -> 0 bytes ...lipse_composition-Dest_blending-Screen.png | Bin 524 -> 0 bytes ...pse_composition-Dest_blending-Subtract.png | Bin 524 -> 0 bytes ...lipse_composition-SrcAtop_blending-Add.png | Bin 675 -> 0 bytes ...se_composition-SrcAtop_blending-Darken.png | Bin 524 -> 0 bytes ...composition-SrcAtop_blending-HardLight.png | Bin 675 -> 0 bytes ...e_composition-SrcAtop_blending-Lighten.png | Bin 678 -> 0 bytes ..._composition-SrcAtop_blending-Multiply.png | Bin 671 -> 0 bytes ...se_composition-SrcAtop_blending-Normal.png | Bin 678 -> 0 bytes ...e_composition-SrcAtop_blending-Overlay.png | Bin 671 -> 0 bytes ...se_composition-SrcAtop_blending-Screen.png | Bin 678 -> 0 bytes ..._composition-SrcAtop_blending-Subtract.png | Bin 668 -> 0 bytes ...Ellipse_composition-SrcIn_blending-Add.png | 3 - ...ipse_composition-SrcIn_blending-Darken.png | 3 - ...e_composition-SrcIn_blending-HardLight.png | 3 - ...pse_composition-SrcIn_blending-Lighten.png | 3 - ...se_composition-SrcIn_blending-Multiply.png | 3 - ...ipse_composition-SrcIn_blending-Normal.png | 3 - ...pse_composition-SrcIn_blending-Overlay.png | 3 - ...ipse_composition-SrcIn_blending-Screen.png | 3 - ...se_composition-SrcIn_blending-Subtract.png | 3 - ...llipse_composition-SrcOut_blending-Add.png | 3 - ...pse_composition-SrcOut_blending-Darken.png | 3 - ..._composition-SrcOut_blending-HardLight.png | 3 - ...se_composition-SrcOut_blending-Lighten.png | 3 - ...e_composition-SrcOut_blending-Multiply.png | 3 - ...pse_composition-SrcOut_blending-Normal.png | 3 - ...se_composition-SrcOut_blending-Overlay.png | 3 - ...pse_composition-SrcOut_blending-Screen.png | 3 - ...e_composition-SrcOut_blending-Subtract.png | 3 - ...lipse_composition-SrcOver_blending-Add.png | Bin 697 -> 0 bytes ...se_composition-SrcOver_blending-Darken.png | Bin 690 -> 0 bytes ...composition-SrcOver_blending-HardLight.png | Bin 700 -> 0 bytes ...e_composition-SrcOver_blending-Lighten.png | Bin 691 -> 0 bytes ..._composition-SrcOver_blending-Multiply.png | Bin 699 -> 0 bytes ...se_composition-SrcOver_blending-Normal.png | Bin 691 -> 0 bytes ...e_composition-SrcOver_blending-Overlay.png | Bin 699 -> 0 bytes ...se_composition-SrcOver_blending-Screen.png | Bin 696 -> 0 bytes ..._composition-SrcOver_blending-Subtract.png | Bin 696 -> 0 bytes ...ntEllipse_composition-Src_blending-Add.png | 3 - ...llipse_composition-Src_blending-Darken.png | 3 - ...pse_composition-Src_blending-HardLight.png | 3 - ...lipse_composition-Src_blending-Lighten.png | 3 - ...ipse_composition-Src_blending-Multiply.png | 3 - ...llipse_composition-Src_blending-Normal.png | 3 - ...lipse_composition-Src_blending-Overlay.png | 3 - ...llipse_composition-Src_blending-Screen.png | 3 - ...ipse_composition-Src_blending-Subtract.png | 3 - ...ntEllipse_composition-Xor_blending-Add.png | Bin 698 -> 0 bytes ...llipse_composition-Xor_blending-Darken.png | Bin 698 -> 0 bytes ...pse_composition-Xor_blending-HardLight.png | Bin 698 -> 0 bytes ...lipse_composition-Xor_blending-Lighten.png | Bin 698 -> 0 bytes ...ipse_composition-Xor_blending-Multiply.png | Bin 698 -> 0 bytes ...llipse_composition-Xor_blending-Normal.png | Bin 698 -> 0 bytes ...lipse_composition-Xor_blending-Overlay.png | Bin 698 -> 0 bytes ...llipse_composition-Xor_blending-Screen.png | Bin 698 -> 0 bytes ...ipse_composition-Xor_blending-Subtract.png | Bin 698 -> 0 bytes ...inkRect_composition-Clear_blending-Add.png | Bin 670 -> 0 bytes ...Rect_composition-Clear_blending-Darken.png | Bin 670 -> 0 bytes ...t_composition-Clear_blending-HardLight.png | Bin 670 -> 0 bytes ...ect_composition-Clear_blending-Lighten.png | Bin 670 -> 0 bytes ...ct_composition-Clear_blending-Multiply.png | Bin 670 -> 0 bytes ...Rect_composition-Clear_blending-Normal.png | Bin 670 -> 0 bytes ...ect_composition-Clear_blending-Overlay.png | Bin 670 -> 0 bytes ...Rect_composition-Clear_blending-Screen.png | Bin 670 -> 0 bytes ...ct_composition-Clear_blending-Subtract.png | Bin 670 -> 0 bytes ...Rect_composition-DestAtop_blending-Add.png | Bin 697 -> 0 bytes ...t_composition-DestAtop_blending-Darken.png | Bin 690 -> 0 bytes ...omposition-DestAtop_blending-HardLight.png | Bin 699 -> 0 bytes ..._composition-DestAtop_blending-Lighten.png | Bin 691 -> 0 bytes ...composition-DestAtop_blending-Multiply.png | Bin 699 -> 0 bytes ...t_composition-DestAtop_blending-Normal.png | Bin 690 -> 0 bytes ..._composition-DestAtop_blending-Overlay.png | Bin 700 -> 0 bytes ...t_composition-DestAtop_blending-Screen.png | Bin 696 -> 0 bytes ...composition-DestAtop_blending-Subtract.png | Bin 695 -> 0 bytes ...nkRect_composition-DestIn_blending-Add.png | Bin 524 -> 0 bytes ...ect_composition-DestIn_blending-Darken.png | Bin 524 -> 0 bytes ..._composition-DestIn_blending-HardLight.png | Bin 524 -> 0 bytes ...ct_composition-DestIn_blending-Lighten.png | Bin 524 -> 0 bytes ...t_composition-DestIn_blending-Multiply.png | Bin 524 -> 0 bytes ...ect_composition-DestIn_blending-Normal.png | Bin 524 -> 0 bytes ...ct_composition-DestIn_blending-Overlay.png | Bin 524 -> 0 bytes ...ect_composition-DestIn_blending-Screen.png | Bin 524 -> 0 bytes ...t_composition-DestIn_blending-Subtract.png | Bin 524 -> 0 bytes ...kRect_composition-DestOut_blending-Add.png | Bin 670 -> 0 bytes ...ct_composition-DestOut_blending-Darken.png | Bin 670 -> 0 bytes ...composition-DestOut_blending-HardLight.png | Bin 670 -> 0 bytes ...t_composition-DestOut_blending-Lighten.png | Bin 670 -> 0 bytes ..._composition-DestOut_blending-Multiply.png | Bin 670 -> 0 bytes ...ct_composition-DestOut_blending-Normal.png | Bin 670 -> 0 bytes ...t_composition-DestOut_blending-Overlay.png | Bin 670 -> 0 bytes ...ct_composition-DestOut_blending-Screen.png | Bin 670 -> 0 bytes ..._composition-DestOut_blending-Subtract.png | Bin 670 -> 0 bytes ...Rect_composition-DestOver_blending-Add.png | Bin 697 -> 0 bytes ...t_composition-DestOver_blending-Darken.png | Bin 690 -> 0 bytes ...omposition-DestOver_blending-HardLight.png | Bin 699 -> 0 bytes ..._composition-DestOver_blending-Lighten.png | Bin 691 -> 0 bytes ...composition-DestOver_blending-Multiply.png | Bin 699 -> 0 bytes ...t_composition-DestOver_blending-Normal.png | Bin 690 -> 0 bytes ..._composition-DestOver_blending-Overlay.png | Bin 700 -> 0 bytes ...t_composition-DestOver_blending-Screen.png | Bin 696 -> 0 bytes ...composition-DestOver_blending-Subtract.png | Bin 695 -> 0 bytes ...PinkRect_composition-Dest_blending-Add.png | Bin 524 -> 0 bytes ...kRect_composition-Dest_blending-Darken.png | Bin 524 -> 0 bytes ...ct_composition-Dest_blending-HardLight.png | Bin 524 -> 0 bytes ...Rect_composition-Dest_blending-Lighten.png | Bin 524 -> 0 bytes ...ect_composition-Dest_blending-Multiply.png | Bin 524 -> 0 bytes ...kRect_composition-Dest_blending-Normal.png | Bin 524 -> 0 bytes ...Rect_composition-Dest_blending-Overlay.png | Bin 524 -> 0 bytes ...kRect_composition-Dest_blending-Screen.png | Bin 524 -> 0 bytes ...ect_composition-Dest_blending-Subtract.png | Bin 524 -> 0 bytes ...kRect_composition-SrcAtop_blending-Add.png | Bin 675 -> 0 bytes ...ct_composition-SrcAtop_blending-Darken.png | Bin 524 -> 0 bytes ...composition-SrcAtop_blending-HardLight.png | Bin 675 -> 0 bytes ...t_composition-SrcAtop_blending-Lighten.png | Bin 678 -> 0 bytes ..._composition-SrcAtop_blending-Multiply.png | Bin 671 -> 0 bytes ...ct_composition-SrcAtop_blending-Normal.png | Bin 678 -> 0 bytes ...t_composition-SrcAtop_blending-Overlay.png | Bin 671 -> 0 bytes ...ct_composition-SrcAtop_blending-Screen.png | Bin 678 -> 0 bytes ..._composition-SrcAtop_blending-Subtract.png | Bin 668 -> 0 bytes ...inkRect_composition-SrcIn_blending-Add.png | Bin 678 -> 0 bytes ...Rect_composition-SrcIn_blending-Darken.png | Bin 678 -> 0 bytes ...t_composition-SrcIn_blending-HardLight.png | Bin 678 -> 0 bytes ...ect_composition-SrcIn_blending-Lighten.png | Bin 678 -> 0 bytes ...ct_composition-SrcIn_blending-Multiply.png | Bin 678 -> 0 bytes ...Rect_composition-SrcIn_blending-Normal.png | Bin 678 -> 0 bytes ...ect_composition-SrcIn_blending-Overlay.png | Bin 678 -> 0 bytes ...Rect_composition-SrcIn_blending-Screen.png | Bin 678 -> 0 bytes ...ct_composition-SrcIn_blending-Subtract.png | Bin 678 -> 0 bytes ...nkRect_composition-SrcOut_blending-Add.png | Bin 698 -> 0 bytes ...ect_composition-SrcOut_blending-Darken.png | Bin 698 -> 0 bytes ..._composition-SrcOut_blending-HardLight.png | Bin 698 -> 0 bytes ...ct_composition-SrcOut_blending-Lighten.png | Bin 698 -> 0 bytes ...t_composition-SrcOut_blending-Multiply.png | Bin 698 -> 0 bytes ...ect_composition-SrcOut_blending-Normal.png | Bin 698 -> 0 bytes ...ct_composition-SrcOut_blending-Overlay.png | Bin 698 -> 0 bytes ...ect_composition-SrcOut_blending-Screen.png | Bin 698 -> 0 bytes ...t_composition-SrcOut_blending-Subtract.png | Bin 698 -> 0 bytes ...kRect_composition-SrcOver_blending-Add.png | Bin 697 -> 0 bytes ...ct_composition-SrcOver_blending-Darken.png | Bin 690 -> 0 bytes ...composition-SrcOver_blending-HardLight.png | Bin 700 -> 0 bytes ...t_composition-SrcOver_blending-Lighten.png | Bin 691 -> 0 bytes ..._composition-SrcOver_blending-Multiply.png | Bin 699 -> 0 bytes ...ct_composition-SrcOver_blending-Normal.png | Bin 691 -> 0 bytes ...t_composition-SrcOver_blending-Overlay.png | Bin 699 -> 0 bytes ...ct_composition-SrcOver_blending-Screen.png | Bin 696 -> 0 bytes ..._composition-SrcOver_blending-Subtract.png | Bin 696 -> 0 bytes ...tPinkRect_composition-Src_blending-Add.png | Bin 691 -> 0 bytes ...nkRect_composition-Src_blending-Darken.png | Bin 691 -> 0 bytes ...ect_composition-Src_blending-HardLight.png | Bin 691 -> 0 bytes ...kRect_composition-Src_blending-Lighten.png | Bin 691 -> 0 bytes ...Rect_composition-Src_blending-Multiply.png | Bin 691 -> 0 bytes ...nkRect_composition-Src_blending-Normal.png | Bin 691 -> 0 bytes ...kRect_composition-Src_blending-Overlay.png | Bin 691 -> 0 bytes ...nkRect_composition-Src_blending-Screen.png | Bin 691 -> 0 bytes ...Rect_composition-Src_blending-Subtract.png | Bin 691 -> 0 bytes ...tPinkRect_composition-Xor_blending-Add.png | Bin 698 -> 0 bytes ...nkRect_composition-Xor_blending-Darken.png | Bin 698 -> 0 bytes ...ect_composition-Xor_blending-HardLight.png | Bin 698 -> 0 bytes ...kRect_composition-Xor_blending-Lighten.png | Bin 698 -> 0 bytes ...Rect_composition-Xor_blending-Multiply.png | Bin 698 -> 0 bytes ...nkRect_composition-Xor_blending-Normal.png | Bin 698 -> 0 bytes ...kRect_composition-Xor_blending-Overlay.png | Bin 698 -> 0 bytes ...nkRect_composition-Xor_blending-Screen.png | Bin 698 -> 0 bytes ...Rect_composition-Xor_blending-Subtract.png | Bin 698 -> 0 bytes 821 files changed, 1351 insertions(+), 393 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png} (100%) rename tests/Images/ReferenceOutput/Drawing/{SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png => ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Darken.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-HardLight.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Lighten.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Multiply.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Normal.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Overlay.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Screen.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Subtract.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs deleted file mode 100644 index f6464394e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class SolidFillBlendedShapesTests -{ - public static IEnumerable Modes { get; } = GetAllModeCombinations(); - - private static IEnumerable GetAllModeCombinations() - { - foreach (object composition in Enum.GetValues(typeof(PixelAlphaCompositionMode))) - { - foreach (object blending in Enum.GetValues(typeof(PixelColorBlendingMode))) - { - yield return [blending, composition]; - } - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)) - - .Fill( - new DrawingOptions - { - GraphicsOptions = - { - Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition - } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.Transparent, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY))); - - Color transparentRed = Color.Red.WithAlpha(0.5f); - - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - transparentRed, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendBlackEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image dstImg = provider.GetImage(), srcImg = provider.GetImage()) - { - int scaleX = dstImg.Width / 100; - int scaleY = dstImg.Height / 100; - - dstImg.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))); - - srcImg.Mutate( - x => x.Fill( - Color.Black, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - dstImg.Mutate( - x => x.DrawImage(srcImg, new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition })); - - VerifyImage(provider, blending, composition, dstImg); - } - } - - private static void VerifyImage( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition, - Image img) - where TPixel : unmanaged, IPixel - { - img.DebugSave( - provider, - new { composition, blending }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageComparer comparer = ImageComparer.TolerantPercentage(0.01f, 3); - img.CompareFirstFrameToReferenceOutput( - comparer, - provider, - new { composition, blending }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs new file mode 100644 index 000000000..50ea36b3b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static IEnumerable BlendingsModes { get; } = GetAllModeCombinations(); + + private static IEnumerable GetAllModeCombinations() + { + foreach (object composition in Enum.GetValues(typeof(PixelAlphaCompositionMode))) + { + foreach (object blending in Enum.GetValues(typeof(PixelColorBlendingMode))) + { + yield return [blending, composition]; + } + } + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRect( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(Color.Transparent)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + Color transparentRed = Color.Red.WithAlpha(0.5F); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + // Keep legacy shape coordinates identical to the original test. + canvas.Fill(new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(transparentRed)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendBlackEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image destinationImage = provider.GetImage(); + using Image sourceImage = provider.GetImage(); + + int scaleX = destinationImage.Width / 100; + int scaleY = destinationImage.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + })); + + sourceImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(Color.Black)); + })); + + destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.DrawImage( + sourceImage, + sourceImage.Bounds, + new RectangleF(0, 0, destinationImage.Width, destinationImage.Height)); + })); + + VerifyImage(provider, blending, composition, destinationImage); + } + + private static DrawingOptions CreateBlendOptions( + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) => + new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + ColorBlendingMode = blending, + AlphaCompositionMode = composition + } + }; + + private static void VerifyImage( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition, + Image image) + where TPixel : unmanaged, IPixel + { + image.DebugSave( + provider, + new { composition, blending }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.01F, 3); + image.CompareFirstFrameToReferenceOutput( + comparer, + provider, + new { composition, blending }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..7bb7d2865 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b +size 371 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..6b10adb50 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 +size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..761b4c89d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:122e92acee8ae979556bdafc6787d98f207bcf2f0fbc7a69a88e4c3b5e65207b +size 1751 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..6b10adb50 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 +size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..6b10adb50 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 +size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..6b10adb50 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 +size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..b794bca24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80057da1abe172d84c830c314dfd93594e2d2671cf1d65ee2f235ad42a15ffe7 +size 816 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..8c0d1942a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9262a38a082952e1f4ccf197c24f50eca407374d8ae0a9d94744217c7db8087 +size 981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..f7603337f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed914df1df4be3d1dad56417b78716bcb7c1b118240eb956cb92413257c66393 +size 1929 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..f3ec6a941 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a8142fe5fe2306cea381863fa10ca4074e7369f6d3230aa12682ceea1264c1 +size 972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..f3ec6a941 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a8142fe5fe2306cea381863fa10ca4074e7369f6d3230aa12682ceea1264c1 +size 972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..f3ec6a941 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a8142fe5fe2306cea381863fa10ca4074e7369f6d3230aa12682ceea1264c1 +size 972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..f3ec6a941 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a8142fe5fe2306cea381863fa10ca4074e7369f6d3230aa12682ceea1264c1 +size 972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..10a1ae29c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ec23afb5c3cefe1028aabe892fbb192f1c5109852735024260c43379a855e25 +size 963 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..e116823ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 +size 780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..e6eab74aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..63d19c050 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 +size 1935 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..f7603337f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed914df1df4be3d1dad56417b78716bcb7c1b118240eb956cb92413257c66393 +size 1929 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..0848b7596 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 +size 1491 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..ee46aaf5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..9dea11fe7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 +size 1954 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..da321f0cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 +size 715 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..25b9ebb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1939c8c6e12888b4d7cd9b97559b0d7a40b6a7f54f878d5c39a0fd5a0969eb14 +size 1004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..b878a6e43 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f063f49ab93cb981cf6191b5d03caf13e4dc09fd73da3236598f3ef54cff6c4 +size 2704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..0c47c88ff --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb8b4e83b35935e0ce500b0753f5666178057556b1222651826a9a8380eb348 +size 3012 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..c255b9188 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba1edb25eed3564259f027828c1e240bd20b69d5d9a9c4e6c74c94a9878e994c +size 3223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..cd1c71dd7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2904aa77ab96ba3460c577fb0c30ca782c9995f42a6ecbf56b744994dc3146e8 +size 2702 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..5537e66d4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f248491c932a51679ff435d04a18db08353284ba17e18ce7fb63e3c9c38007e9 +size 3249 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..482e86051 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf887cd655d8f0fae2e2e77cc0bb2f06e472d26da21a2c85d1fc803114246a54 +size 2204 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..797a55a9b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a0cb483159c337df967e4149686715b258e01ddb7f6594da03916aa64257364 +size 3311 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..9f3e8dd3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cacf851a38df21eb75b435216a235699a71da5a3489e9702a21d59e4d15563 +size 2707 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..824249ecf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67d52bc2bbf96770536d1599f02dcd315a4235c1aaa6bdd523008e4c80240748 +size 3478 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..97f49f609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..5bd706320 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6fcecae6f0cc30ad76eece9d90ea4120fb523f36048252e030e3ae26a72feaa +size 1081 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..724671eef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31b60bf743e06c65fb1f4e80ce5fb1b96219378534a4d8f3186c7a15d71cc195 +size 1055 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..99589a939 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb33d5a5f24f60e182b3b5af37d1f3682d4ac7b6673ecbcaf30694ef45c84c2e +size 1393 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..62033d730 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18da21ed099376868394db34d3f02fc6229278a4c1eca61d4400631b08415813 +size 1084 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..091fc37a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95977b134c222f40f06a4e3f5d287e3f4ea52c3d6d18b8ffccc6ec878ba9b02c +size 1305 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..d737910a0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5377bac8bac14130ee8ad758b929d9a09b5e7379551716fb1c8d10b842b6ec4 +size 1441 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..a73417168 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a031832b3b3287402122f31c790e951f9f0e204a512f4282fb4efe37a5aa8e8d +size 1290 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..c908b8124 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edc855624619de74171b4aad783c8f51120c18579bd178d34c9a1f8ff8850ea8 +size 1084 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..084ff1bcf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c36ad06ee6b1f2e1cd527f25f707beb3cec0d32bc87dc1e527b9a4844268b5 +size 756 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..b878a6e43 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f063f49ab93cb981cf6191b5d03caf13e4dc09fd73da3236598f3ef54cff6c4 +size 2704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..0c47c88ff --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb8b4e83b35935e0ce500b0753f5666178057556b1222651826a9a8380eb348 +size 3012 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..797a55a9b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a0cb483159c337df967e4149686715b258e01ddb7f6594da03916aa64257364 +size 3311 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..cd1c71dd7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2904aa77ab96ba3460c577fb0c30ca782c9995f42a6ecbf56b744994dc3146e8 +size 2702 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..5537e66d4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f248491c932a51679ff435d04a18db08353284ba17e18ce7fb63e3c9c38007e9 +size 3249 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..dd9dbe563 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:950dfc002a89ff2bc96d0abe9b43b5573c3e60d0c0b3abe85035f286ba1e7af4 +size 3339 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..c255b9188 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba1edb25eed3564259f027828c1e240bd20b69d5d9a9c4e6c74c94a9878e994c +size 3223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..9f3e8dd3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cacf851a38df21eb75b435216a235699a71da5a3489e9702a21d59e4d15563 +size 2707 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..a2661c8a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:545ce1c6eec35fbdd16e70f8b02a6dbfeacd53d52f991e479e1da5dad706dc17 +size 2719 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..b0ecbb4d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937f7b62b22acfc9c712a9a2e62f23307ffba52701e860d3c5af9188402fe437 +size 3214 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..22d5914fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a +size 1624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..aaaa001c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..09881b5ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..111778ee8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 +size 1625 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..2a0453172 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..cc67c91c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..f30bc174e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3570b7e310568379e9e66e35929de9d2932444d8a2add05141ba072b1d08bda5 +size 760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..1c6c67d9a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f48589e6282a0e0c43685d72ba2ce1ec23de54b3e1dfad07089e173098528be +size 760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..cb9431bb2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2659722e58d9506e9469bf8d11bef52dbbb85734c378557a5b35bc9a1f85983 +size 757 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..e94068a00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f268d005f5118998bb5c9ca7b382fc09ea2fe0738952256736b2bdf4919e44e6 +size 757 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..aa07708ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a08118f15161a1ba8905a8b9918b3badefe52cab38e4b868006451746a444f1 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..1b1953c54 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2271351e9a8106eab04b74b2909885620ba8c2f141cf40208133c955344a96b +size 755 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..442db9eb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e +size 694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..22d5914fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a +size 1624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..111778ee8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 +size 1625 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..09881b5ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..aaaa001c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..2a0453172 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..b524d9dc8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d6dd72ffd135a849125c3938f84e3355020b5e88508e9aa66ebaa746ea1cbc4 +size 1622 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..b8535b73b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 +size 1524 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..22d5914fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a +size 1624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..aaaa001c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..09881b5ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..111778ee8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 +size 1625 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..2a0453172 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..cc67c91c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..cbfd01277 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee +size 678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..22d5914fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a +size 1624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..aaaa001c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..09881b5ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..111778ee8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 +size 1625 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..2a0453172 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..cc67c91c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..f30bc174e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3570b7e310568379e9e66e35929de9d2932444d8a2add05141ba072b1d08bda5 +size 760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..bb11e6217 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec +size 519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..1c6c67d9a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f48589e6282a0e0c43685d72ba2ce1ec23de54b3e1dfad07089e173098528be +size 760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..cb9431bb2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2659722e58d9506e9469bf8d11bef52dbbb85734c378557a5b35bc9a1f85983 +size 757 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..e94068a00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f268d005f5118998bb5c9ca7b382fc09ea2fe0738952256736b2bdf4919e44e6 +size 757 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..aa07708ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a08118f15161a1ba8905a8b9918b3badefe52cab38e4b868006451746a444f1 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..1b1953c54 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2271351e9a8106eab04b74b2909885620ba8c2f141cf40208133c955344a96b +size 755 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..0ee00c746 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 +size 762 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..22d5914fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a +size 1624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..02c032247 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f +size 1388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..111778ee8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 +size 1625 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..09881b5ee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..aaaa001c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 +size 1623 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..2a0453172 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 +size 1626 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..b524d9dc8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d6dd72ffd135a849125c3938f84e3355020b5e88508e9aa66ebaa746ea1cbc4 +size 1622 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..8f661c054 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 +size 1620 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..50c109be4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 +size 1552 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png deleted file mode 100644 index c7ab88be3cb576be676d4bb0fa165b51ea7c37d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4V<1Xjv*CsZx1pu0+lo={9m8v;Q(ZfFc7nf Y@#1y{Mw4d=pkQF|boFyt=akR{0C&bB2><{9 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png deleted file mode 100644 index a5c63850515fe29ebeb6151eb6896ffc073e0fc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 zcmbVMeKga17~d4POr+a&iuL|-ySH#z$IG<2iZN!BZt}X>6(g@TFSj3?TM{=RN8Vp@ zTV8H4G**O(khh6p6NXC6aFUHP`*-(y&hvdf&pDs-oag&_KIi+F1J-(nsJYi6XYC$nW@BSA;x)$&S`%L3B1$ATPF(EbJ{npvq*(8c|tZZx69?A%j5b z&58iG1(#d~fm9BlEiHcg13x!fpWq#_uWi9Yx2nRy;#C3BO82hzPK!WHiqj{J3eBxA zowuoKa-4bXmFKr69^AF_Xspf8+j+jlPITA8*S2aZhfmx-@f1lx1{`f38mrZSO-=Au zftNXrK1*!Vu|-7JJbNTkA1t5Zo6fUOts)2DC`A{MMa`#X$TDSXw(Hj?S<-45i;6L4 zvWi*Hl6%{f!pCKzERiK}T&&XYT9Qebr6y7JJdYi)A=tCfv>bA~#B{YLQms)rk}Qd; z%|;oIYl)o!48mv}%1>=b!((YVff`~Sa2Me;u2*M9F_*#;gXoAsuQ`Kd-i`J5_5vLN zpgoF-b4aeAdP4@YpXVa;WcN|VJ%v8VkkY97MHz5PyG9XqZ!l( zsD!c{VJRGDvZox!vZciimI=GyW+vOl>}XblbHZ8AWn>x(QfgKXyEO`2`W#rb$lV*B zUSURof9&b=y&AS0mY)BvV?N3#QOju=Pf~rckS=0z?YJ!iS>enI$QYVtHLxNKAd)p6 zodY!6&OKqw-6dWy$Iz4qS_bwu`|uyg(omiMBs_u5mlD6HDh1WAy@I-B^0Q?rPj9*Y z&4$C`F4_~2j9ae7T~H^O2ev2I<=3(cr1|1W=T1>Ctg|?*bC&o6hfC@#7I!ERgE7eg zyq7AT3+fpy9Lc11+1*W~eL6eT#FKC~2t4@5MD; zPfbo6sk3OVqxw%P3?W-Lhk25#q-8*fWCpyYyF;8`iBgB2ZV<~iiP4!~iPFHY#E^+9 zmx}maIoVT_=FJIzc^D<{d(#RN&AL8Rb-qTcB3`hv*=t}EaTs#)q-*2hqW=a-?oPKK zPV)yRWFigRi~=vwl~4U%Yq)`YfUC@qFR$nq^#=&5s5L4TC4%0eK0oQT9iHboF^s}5 z61*##@=4eb$aqt#HlMGe`S(>Tq@3x3MLKtj(qNF1u#PU`Mb0os46`fo7M8-u&SM&m z=?*9RU7C+0@5}N~2^&j;nPX{_{fcE4Av2>c66kCu>~=RI1!ebmkvr6+HoW0Wu$j_vPeiT+&!;JK z6Nf@kA)h2m7C}onLQGxtF6%-<^_qcf01hFvo7zevOQH*27;K6DajHa2NXM0<7lKFF;}uKF_7|P`7%BdbpY~B h|LFfA8-)n^qQSqI2s6Ihc`{GY0d0k~q@VS?`7ayWCtm;n diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png deleted file mode 100644 index 131d4dc8a19f84eb09dcc2dbeaa1a3299b0bb4ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1673 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 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png deleted file mode 100644 index a5c63850515fe29ebeb6151eb6896ffc073e0fc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 zcmbVMeKga17~d4POr+a&iuL|-ySH#z$IG<2iZN!BZt}X>6(g@TFSj3?TM{=RN8Vp@ zTV8H4G**O(khh6p6NXC6aFUHP`*-(y&hvdf&pDs-oag&_KIi+F1J-(nsJYi6XYC$nW@BSA;x)$&S`%L3B1$ATPF(EbJ{npvq*(8c|tZZx69?A%j5b z&58iG1(#d~fm9BlEiHcg13x!fpWq#_uWi9Yx2nRy;#C3BO82hzPK!WHiqj{J3eBxA zowuoKa-4bXmFKr69^AF_Xspf8+j+jlPITA8*S2aZhfmx-@f1lx1{`f38mrZSO-=Au zftNXrK1*!Vu|-7JJbNTkA1t5Zo6fUOts)2DC`A{MMa`#X$TDSXw(Hj?S<-45i;6L4 zvWi*Hl6%{f!pCKzERiK}T&&XYT9Qebr6y7JJdYi)A=tCfv>bA~#B{YLQms)rk}Qd; z%|;oIYl)o!48mv}%1>=b!((YVff`~Sa2Me;u2*M9F_*#;gXoAsuQ`Kd-i`J5_5vLN zpgoF-b4aeAdP4@YpXVa;WcN|VJ%v8VkkY97MHz5PyG9XqZ!l( zsD!c{VJRGDvZox!vZciimI=GyW+vOl>}XblbHZ8AWn>x(QfgKXyEO`2`W#rb$lV*B zUSURof9&b=y&AS0mY)BvV?N3#QOju=Pf~rckS=0z?YJ!iS>enI$QYVtHLxNKAd)p6 zodY!6&OKqw-6dWy$Iz4qS_bwu`|uyg(omiMBs_u5mlD6HDh1WAy@I-B^0Q?rPj9*Y z&4$C`F4_~2j9ae7T~H^O2ev2I<=3(cr1|1W=T1>Ctg|?*bC&o6hfC@#7I!ERgE7eg zyq7AT3+fpy9Lc11+1*W~eL6eT#FKC~2t4@5MD; zPfbo6sk3OVqxw%P3?W-Lhk25#q-8*fWCpyYyF;8`iBgB2ZV<~iiP4!~iPFHY#E^+9 zmx}maIoVT_=FJIzc^D<{d(#RN&AL8Rb-qTcB3`hv*=t}EaTs#)q-*2hqW=a-?oPKK zPV)yRWFigRi~=vwl~4U%Yq)`YfUC@qFR$nq^#=&5s5L4TC4%0eK0oQT9iHboF^s}5 z61*##@=4eb$aqt#HlMGe`S(>Tq@3x3MLKtj(qNF1u#PU`Mb0os46`fo7M8-u&SM&m z=?*9RU7C+0@5}N~2^&j;nPX{_{fcE4Av2>c66kCu>~=RI1!ebmkvr6+HoW0Wu$j_vPeiT+&!;JK z6Nf@kA)h2m7C}onLQGxtF6%-<^_qcf01hFvo7zevOQH*27;K6DajHa2NXM0<7lKFF;}uKF_7|P`7%BdbpY~B h|LFfA8-)n^qQSqI2s6Ihc`{GY0d0k~q@VS?`7ayWCtm;n diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png deleted file mode 100644 index a5c63850515fe29ebeb6151eb6896ffc073e0fc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 zcmbVMeKga17~d4POr+a&iuL|-ySH#z$IG<2iZN!BZt}X>6(g@TFSj3?TM{=RN8Vp@ zTV8H4G**O(khh6p6NXC6aFUHP`*-(y&hvdf&pDs-oag&_KIi+F1J-(nsJYi6XYC$nW@BSA;x)$&S`%L3B1$ATPF(EbJ{npvq*(8c|tZZx69?A%j5b z&58iG1(#d~fm9BlEiHcg13x!fpWq#_uWi9Yx2nRy;#C3BO82hzPK!WHiqj{J3eBxA zowuoKa-4bXmFKr69^AF_Xspf8+j+jlPITA8*S2aZhfmx-@f1lx1{`f38mrZSO-=Au zftNXrK1*!Vu|-7JJbNTkA1t5Zo6fUOts)2DC`A{MMa`#X$TDSXw(Hj?S<-45i;6L4 zvWi*Hl6%{f!pCKzERiK}T&&XYT9Qebr6y7JJdYi)A=tCfv>bA~#B{YLQms)rk}Qd; z%|;oIYl)o!48mv}%1>=b!((YVff`~Sa2Me;u2*M9F_*#;gXoAsuQ`Kd-i`J5_5vLN zpgoF-b4aeAdP4@YpXVa;WcN|VJ%v8VkkY97MHz5PyG9XqZ!l( zsD!c{VJRGDvZox!vZciimI=GyW+vOl>}XblbHZ8AWn>x(QfgKXyEO`2`W#rb$lV*B zUSURof9&b=y&AS0mY)BvV?N3#QOju=Pf~rckS=0z?YJ!iS>enI$QYVtHLxNKAd)p6 zodY!6&OKqw-6dWy$Iz4qS_bwu`|uyg(omiMBs_u5mlD6HDh1WAy@I-B^0Q?rPj9*Y z&4$C`F4_~2j9ae7T~H^O2ev2I<=3(cr1|1W=T1>Ctg|?*bC&o6hfC@#7I!ERgE7eg zyq7AT3+fpy9Lc11+1*W~eL6eT#FKC~2t4@5MD; zPfbo6sk3OVqxw%P3?W-Lhk25#q-8*fWCpyYyF;8`iBgB2ZV<~iiP4!~iPFHY#E^+9 zmx}maIoVT_=FJIzc^D<{d(#RN&AL8Rb-qTcB3`hv*=t}EaTs#)q-*2hqW=a-?oPKK zPV)yRWFigRi~=vwl~4U%Yq)`YfUC@qFR$nq^#=&5s5L4TC4%0eK0oQT9iHboF^s}5 z61*##@=4eb$aqt#HlMGe`S(>Tq@3x3MLKtj(qNF1u#PU`Mb0os46`fo7M8-u&SM&m z=?*9RU7C+0@5}N~2^&j;nPX{_{fcE4Av2>c66kCu>~=RI1!ebmkvr6+HoW0Wu$j_vPeiT+&!;JK z6Nf@kA)h2m7C}onLQGxtF6%-<^_qcf01hFvo7zevOQH*27;K6DajHa2NXM0<7lKFF;}uKF_7|P`7%BdbpY~B h|LFfA8-)n^qQSqI2s6Ihc`{GY0d0k~q@VS?`7ayWCtm;n diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png deleted file mode 100644 index 438d112552056e1464f95e6d43492cdee854b7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3{1tIE{-7;ac?gfuIpnIXuEh?%|s=tH!7J` zs5($UN?Ky7n~u{$q0pF6=XEsw+^zpu|E$l$IZtjLI8)6CG?secfB%kYf6vrq zFS)$-^D_f2IeE7gGe12uP+ONG&&PblXZh^sZxdbaSoZjDehOsRXg|IjQ&}w`I6bvy zmUce>+ecxN^PZiScUWB!;I{ndC-w_kaZ@j5?)$^IrE7g^$t?YPhTKPPl5@VDig)PV z5q@mh-cRBel;oyv%v}G6bxVhMYR)a~dPdzxzLK-SN-AJV?50DNyoV||$LE*r*j=1_ zIqt%vJ2$q7Z?P~pd&{c(==lBQ%RxW6GX&#L-PjWTk16WNZzHp}Q};KBecaaLTm4jj zf$JU}pe~RSf0&ZLP$h+%AxeB}w3{#2{AA7$U4Qz8N&G*CsH2CCtjg-Q)N9 zDgT1l8m+?+ub)5t!zBEkoZFGRMs{bXZf_SXH2qy6F+a`fu1!9F@}rlD4YM@!`4&H_ s`u5tu>1)}p(t9%0i!O3Z`q$gtZ9lWF`jWm&b_~b>Pgg&ebxsLQ01rg&AOHXW diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png deleted file mode 100644 index aadf1c064c5e8313935b8d6a05d68dc8da9bfaf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 939 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-49pImE{-7;ac?i$u38!@(E3oiW6Ms36$=_y z@;O@>6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx6$m6HJ>l>X5uMZhfWxQ9+E8@YO{X46 z`}60$?3Z5~xBqtBQ~34z_4&(nzbrAmRQUMsh5kgJzm<2K{G*g*&2QQF*Ioh%{QUYo zQ}`}eNdB+Dv8(rw`<7()mfXp>|IJwcRZ?>Ja+rF7MZgVnX?OtQ+1F3!H!!>c}r|B z6)M+WL*`sS;_SVQ;0PDSUvhhO{-%Pgx!`xz+$I0!@Lnp!DyVGxn!O+D+{+yk z@4ZLj*u6#OAj|A~g(R~NS*8wI&p#w(8}Ge!FYw6~*naV<)XN zSZ3C7`$ZNg7UWT5;WBb8e5+?)*TTbdFIx{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png deleted file mode 100644 index e98bf8cec83f4f18bb833992ed80cb235693a233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1945 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 K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png deleted file mode 100644 index fdf7478512e931016da00e5e590ab4b1caf7af0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1461 zcmdT^`%hB`6fT>{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png deleted file mode 100644 index fdf7478512e931016da00e5e590ab4b1caf7af0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1461 zcmdT^`%hB`6fT>{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{}f^wJI#X}j3D%*dmoH)HBKw+&wR&YZe+%A7mw zRzz2?#=#CoY0j#q%iI%-&!wg5&bYVxe@$F((fbXIR{k#(N*rD^Twq+t?8M%}#~~|V z1;QwTFbSAo^M$z#5T!5?pk9Q2h@c~?p$a8vh7xHL)Lln7{yy9NuYIH6`O+#gG0h}< z+whl`C0+Khe7im@ztR0M@lMyC6~5KQzmyza+}76+o2HR>@t)j5@sCsHEmiG`-nQNS zh3lQ@gKuBfy(kYWsNN;}E4wNzY(?7DyoL*#t2Q{4nh8{u^^2`I$5w0bMM>D|pWFEt zdw>4zwRjb^JzC+5&b(NAo@fr++2Ysv4~y+{KmY9P*TWlHFYMjX(s!Z!`@>&}H=JJF ztl3p3u}xr?`1A{czuxVO0qFn-r9wm3e#z&>J?FoFQTW0PGQsJ5+7pPLALp4LZD`Fu zx5={TuR)IDmmIZq@@={;^UiL3-JZz3FunK{P~D=}TKgoo|1G+>d+*oYnwNTC?kod2 z)#*FXrB46%8*_)GGcQappEPfy=qkzW(h6VRoLl?gTZ#P_ZtWemclm2?{rKp|JNcM& zp_6{bgn2VnyJl|%2FTGfk83pc&a8dMWBVxB>PthtGY1*5x=R=(@qi*9DOC`eqG+Ce k5NVolx`YM7f?e`!9+<6D`4IgBm;)I+UHx3vIVCg!0M{9MBLDyZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png deleted file mode 100644 index e551a730268a88cf88a0b8984802c0e466351666..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{}f^wJI#X}j3D%*dmoH)HBKw+&wR&YZe+%A7mw zRzz2?#=#CoY0j#q%iI%-&!wg5&bYVxe@$F((fbXIR{k#(N*rD^Twq+t?8M%}#~~|V z1;QwTFbSAo^M$z#5T!5?pk9Q2h@c~?p$a8vh7xHL)Lln7{yy9NuYIH6`O+#gG0h}< z+whl`C0+Khe7im@ztR0M@lMyC6~5KQzmyza+}76+o2HR>@t)j5@sCsHEmiG`-nQNS zh3lQ@gKuBfy(kYWsNN;}E4wNzY(?7DyoL*#t2Q{4nh8{u^^2`I$5w0bMM>D|pWFEt zdw>4zwRjb^JzC+5&b(NAo@fr++2Ysv4~y+{KmY9P*TWlHFYMjX(s!Z!`@>&}H=JJF ztl3p3u}xr?`1A{czuxVO0qFn-r9wm3e#z&>J?FoFQTW0PGQsJ5+7pPLALp4LZD`Fu zx5={TuR)IDmmIZq@@={;^UiL3-JZz3FunK{P~D=}TKgoo|1G+>d+*oYnwNTC?kod2 z)#*FXrB46%8*_)GGcQappEPfy=qkzW(h6VRoLl?gTZ#P_ZtWemclm2?{rKp|JNcM& zp_6{bgn2VnyJl|%2FTGfk83pc&a8dMWBVxB>PthtGY1*5x=R=(@qi*9DOC`eqG+Ce k5NVolx`YM7f?e`!9+<6D`4IgBm;)I+UHx3vIVCg!0M{9MBLDyZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{}f^wJI#X}j3D%*dmoH)HBKw+&wR&YZe+%A7mw zRzz2?#=#CoY0j#q%iI%-&!wg5&bYVxe@$F((fbXIR{k#(N*rD^Twq+t?8M%}#~~|V z1;QwTFbSAo^M$z#5T!5?pk9Q2h@c~?p$a8vh7xHL)Lln7{yy9NuYIH6`O+#gG0h}< z+whl`C0+Khe7im@ztR0M@lMyC6~5KQzmyza+}76+o2HR>@t)j5@sCsHEmiG`-nQNS zh3lQ@gKuBfy(kYWsNN;}E4wNzY(?7DyoL*#t2Q{4nh8{u^^2`I$5w0bMM>D|pWFEt zdw>4zwRjb^JzC+5&b(NAo@fr++2Ysv4~y+{KmY9P*TWlHFYMjX(s!Z!`@>&}H=JJF ztl3p3u}xr?`1A{czuxVO0qFn-r9wm3e#z&>J?FoFQTW0PGQsJ5+7pPLALp4LZD`Fu zx5={TuR)IDmmIZq@@={;^UiL3-JZz3FunK{P~D=}TKgoo|1G+>d+*oYnwNTC?kod2 z)#*FXrB46%8*_)GGcQappEPfy=qkzW(h6VRoLl?gTZ#P_ZtWemclm2?{rKp|JNcM& zp_6{bgn2VnyJl|%2FTGfk83pc&a8dMWBVxB>PthtGY1*5x=R=(@qi*9DOC`eqG+Ce k5NVolx`YM7f?e`!9+<6D`4IgBm;)I+UHx3vIVCg!0M{9MBLDyZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index e551a730268a88cf88a0b8984802c0e466351666..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{}f^wJI#X}j3D%*dmoH)HBKw+&wR&YZe+%A7mw zRzz2?#=#CoY0j#q%iI%-&!wg5&bYVxe@$F((fbXIR{k#(N*rD^Twq+t?8M%}#~~|V z1;QwTFbSAo^M$z#5T!5?pk9Q2h@c~?p$a8vh7xHL)Lln7{yy9NuYIH6`O+#gG0h}< z+whl`C0+Khe7im@ztR0M@lMyC6~5KQzmyza+}76+o2HR>@t)j5@sCsHEmiG`-nQNS zh3lQ@gKuBfy(kYWsNN;}E4wNzY(?7DyoL*#t2Q{4nh8{u^^2`I$5w0bMM>D|pWFEt zdw>4zwRjb^JzC+5&b(NAo@fr++2Ysv4~y+{KmY9P*TWlHFYMjX(s!Z!`@>&}H=JJF ztl3p3u}xr?`1A{czuxVO0qFn-r9wm3e#z&>J?FoFQTW0PGQsJ5+7pPLALp4LZD`Fu zx5={TuR)IDmmIZq@@={;^UiL3-JZz3FunK{P~D=}TKgoo|1G+>d+*oYnwNTC?kod2 z)#*FXrB46%8*_)GGcQappEPfy=qkzW(h6VRoLl?gTZ#P_ZtWemclm2?{rKp|JNcM& zp_6{bgn2VnyJl|%2FTGfk83pc&a8dMWBVxB>PthtGY1*5x=R=(@qi*9DOC`eqG+Ce k5NVolx`YM7f?e`!9+<6D`4IgBm;)I+UHx3vIVCg!0M{9MBLDyZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index ff8fddbe9f8db5a09277bb888a03edceb77ddf6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 954 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Id$~7srr_TW{|i)XPp3X?rNfEV`Ccn8!zA>-DExF|FDKv2IC8 z4m=aKct~daI6BW;CHw#Bd*>&=sXYH{kFCtvn-UCj)k_4d6iOUkG+ba@$n3=4!p9*C z!YG0;37DYc3qJ;kQkV$i!e)eih#;Dw0#;~-5@{3ET}3T*|04gV>YOePoHx}qYGd5I z%b#*AkFG!07W*T3&C?3W=}P%mSM9W{ov2ZAXAWO`UHrVAs!`_odHoln3sYIQFaNzT zKKkI>mt`;fpGHP3oaMTi(TTq_x$#ww!k2>WVk^$E)du{WTv!fb{d430YV_l$i%hBR zY+Zq>4)*K*hsB)uKfm~udE?Oe7m0tOborw>Y|m_dy*!b7VS8oiUy~fgF9!MR=C|p# z$UVFJ_40<+3x{iV)JbiF1cd{WsJ-NK+hf0NZ8>T$gQaT=Ib|3Azq<`&g#Db>nPA0L zGh`uFZGH{3%IU?v9WCefhkulRyl;Q3Oyq)H6Z7x3%H`f?x_Z;$#X~;bfVsRaa_>$+ zxZh40Z*wwzoW=|ky1|v_|J~VsW7F-+e=qQ}*)>HTUb55HDgWpO8)NS+dDrg9E!>_q z>-o|zY2Q{Ff9cp>@`CYyy8uS~5D~@d447#HDJ6ltk4Pf8l2;2K5jLTuP~yx*cN!^) c)S)y!Zj;HHkFIi6z|6FVdQ&MBb@03g0_?*IS* diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png deleted file mode 100644 index 6b582928bf6eba82b73ab0d17e0ae6b5821fe9c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-3``ZCE{-7;ac?gv=7};0xL%AmYt-ZvYd*Gt zY0^7ZM`vE`6^8`G8XYF;G3zv`wYIRtI|)4PmA^St-$H$-^o&Y&pt;loKl*Ka>MzX= zTNQo%Y~t0|S9pVpubxdj>-lTeF2k&KyDIO>h1q!D6)=qlGGdnHwai+d$FIF_DTMKO z-jckATdV6B7RN2is{u3YmOvSGP?2Q0rFj*%*4HsA$1TY#0W;18n_ImN-`^nkJ_yQ~ z4`;xn?!y?{S4z+QwmSYmM_z$doWUaL*?rBv0+cg!lcL_om^I(h#>w?W)&W4(=zkPU?`AX@<&sNPoyz257^U~%k pf5W%0o6StUC?mt91A8rG{xRBaQ=4>KuT%(RfTydU%Q~loCIE)*?}Y#W diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png deleted file mode 100644 index e75c117cfedefa9171c4128117d51ba892f331f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmbtSi!<8?9FNXBhgvrq@u?DhfdFN3_XZ5JZc_a~!lB7`zc(?tElc%-AFg~j-B--nXDY`EiY^)MHB8Ay^dMmb;hptqW^ zlj%EIa+#xP`+u?(+k8Byl{E92nr`%BHHa@@Q#F6`v3tyeCuMkSf3spzpJC{(uo=K$ z8i;&B+=Z2|l~P^CZlE!F#BB(sl-iEz`#HiIe;Q3jG+!1Nl^qs)99u z7TuTsG%Z6`4)ufGREwPz&_Fc8*+lSDW#83_%jivPezc$!N}->dcmRr|QyFG&*T^O3 z4(h{CPr7Qm4UHwa!eXf+fbnS3FtW`(gY%)&+K?gM1^9rQ%o&v0 z!#0!#iVI+kdn8?U9+3>41_-80i>nHPd*WjoklHi$}p4 zMdBO)$76$Ci^W+aWl6(bzj-DYP!#6cOZQ>bg~O7Tw4JDcMz&V5PI>%*v4-lE+h@Ph zBUHbr2IcnN8`TKah>8Vx;+23*FujvXT1XgsZ76!dQIJ=2HW+u#6lN9OC-Uq%Is^fg zvVl(yXg|}tK>6d|%BW&E8`*B^fKr*KCe+@yX&t@#n)I$^9M7(OZqE6c>UZv2Ja3}S z6f>1Vr)4u{@inRIyD2+i(_7{xb+z_PovS|(MgRp>k4x3QJfwN5}NtbH|CbLKIpi{rW5KX z2-Se;>~xY%-FB`^UW#5UKklt3qkwSOCG`9vriBL_c zQb=?6Ms;7*o8CgATto5Xw%OWDBZiF}=+9O-kLoaYnsiG?!Yq8untPMAzlMSeUgx%y zd1$QYNdcRfKv)+xA?1x=xkOhsBtKc0yWqgGp{^9w1Lto^3;4fl1=Pq%=XaPmTuC+mkYJg<(1-9Edwz`7muk-wQVPGDJjR&*~2Pnuy;=-bJM v;GvSEmhRAugRGh$r*~8Ke^hmj*22r*BNrA(lAgCr9u0~LBZh(@Y2W<|Zfhqt diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index fdf7478512e931016da00e5e590ab4b1caf7af0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1461 zcmdT^`%hB`6fT>{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index fdf7478512e931016da00e5e590ab4b1caf7af0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1461 zcmdT^`%hB`6fT>{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png deleted file mode 100644 index 6589f4b4b703aa6c0b0546172122aab9c9772fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmbVNc~nwa7{@jmE89+17G=ljv{|H%+DW0KS=wSQ2?#FeF(4yRL!<(wCMWB-O`D3j zlsbwJLyJfSHLV#fLCxyPaX~8s>?u;37H-YM`KSD^dFR}B@AvMv{OGA9G(~ zYGH~%AeJ2RaPx)dapPFH0DfN?Bw^smgz4+E2*mxgMU$cPVfmLR z4@3t04_w?oP zIW5`=Oy0=Zx|N}t9_Hh`FWM1xivpuPYsC6f&z>})PzLDn`vkcq!KLx!slU!o>82;f zcq4Q}4fiP4A@4?ObJ+X)7gl}sAJsu0prts*o1pYYMVOZUo^Eof8;=Q|dIT|A;?m3b zWjS<2>|iHU52*o}Nyb*?b$XM;r1)bA)Ck_jvBmf91_p?1l%|0RUif`w(>Ru+(n2oL zWHEJhoXVo{&`~_7I-2M$!qA_QE_`DWS>#a?sB%v9Z@+F&iHQ2l0ODaA`C>{0(Q5s@ zNI#xXy6!~LgND*osZB^A52y09VG^0`X0K$m=Sv_)LfBS%>*5eng=h2xZDjHfQRgDjy^=UnBe- z{U<{(?ax96Xw!)lYSeB~Y5Z{(lB7l3$p_z9I^$SK;+Bk#dMHi|nU(LVRxHC0th}|c zAg5NbTbuwsm)nqGx56Z}`11+^Uh9Nohp;|SipnjNIDIu)8cU#Nbv)TVSqPZ{$H>%0 zNwTWFg^tnA4{f^?Ys^RGfYY7MAij;YrXA|VvG+)1wtWf<^S5iG`i7IM`bJJfOuoe6 zgM1{3zD{Uyi$YfOqQkQwpu4}8#6tQ`?6A|1xzZd{7|~Xo{PB|@>Y%Pn;VjmVxpft_ z?PQ!->ZfvfJuM%UVcH&r$0V)mP~q4MCEsi(7+wz}EL86f#(IJ%SKVVpZP&_(+XpKu zeK3J6q<3G_h8RwP=c|5mM}Q+7`WPPAafuQ6Fx+Ut3II{qlpUNGx)cW^5&<@lmFV@I zMZO-Vs(m=tVfUBZ-mbH^M(qQMo+s z{%jT6Y?UA~EAd#LJUE6^nAK`6{U@d03dipMt#6xt}aEc;{;2`3e zAgn}BIjFmK%0UbtEPtpxyh@7kZKj&Tr+STY*hzr>bo}Rsz(W2Q3ILp9EXZT|T~;8M z3TM_Wcegm#IaD)gOu-dw&}?X_v-Qa((*4DsC}?9unla+%d$U#2@T$At=dEBo1VlN1 zpK3;l^l5Q4WvR5Lt;kr;TVGNFu=>?sdS(i-0r+v3&(0$5(pxjn80+`|jZ{d+T>2 zAP*P{ImM=l|aLS$`HpIN8hbDH2o0CJMy$GsJ;0O8X&{Yh9@iGEO(00wIL+y>o zjq5*fRr%vA5al1l>v?S1ijMwmvTw5vIH0Pbw(O14!W z>G9kgnG^F&?~*}pwWzjRoNXC-Ig^dW6t2T6v*=?7Q=_g82Wt!)S69z;%_fQ3qDH?r z`(+9k2JV1c>rNlhX5~%#+}QqMs0DHd^MIiR-84g%p|k38;G*4iG-0Ap3p~cFn$Kko zXPz76rf_$L%x3_@r^?-dJbWtwP2h>7gjTh3m}AJ~p2lv;yWRLK%;cwjCjUdZZca1&v&R4c diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index e98bf8cec83f4f18bb833992ed80cb235693a233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1945 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 K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index fdf7478512e931016da00e5e590ab4b1caf7af0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1461 zcmdT^`%hB`6fT>{qjTbnRBf4=8>^d8kRk|`86E-!xq@lAlokOiAY&>}TDT|=#i0hR z@@zUN8)G5R3X~!Ut+WEt#v(YB>p*)^91p$TRwL`&I*KcQg8OYJIp=&|^5vZ5JIT2i z3q-7Q-s+6Q;nvY3!w%WHWi=hw+Uf-?G04{Jc!wfFa2?~M7dEmcBRD1)hwHk6Urx2R zai{D^77vH}cz8AKbSGPn;&Ap+^srz?!C@m3RKZExteXwPdwMYB-WS3qzf_kxCT3Qy zf9fFLuqN2kV-22&r*$3jV7NFrRz-Q(6OEIUZ__qzh>MN=Xkxn>DQS$nZOF;*?iN$; zaR|#URfw4Pq;WXCn@T=!+x}ir8r`QZ>?@QOwgs|^I7O@?(3+>#jKA~O)IAy1&2g6k z{x<9l{}i84%9fF(8)1-1dCIKQ-Q_L@yy4v1lzY&8#0OBb;7msOCG0K3G zN?-2KRdMxQcK>8&m=pQED5clEhC8}zA>m9=Uvr_#GJsM#-0QewyB4R;yvqqSNAWvQ zN=wWRUE>xX{A;iCGM#Lj%=B#fwoi1r{yVu@7{4tZ8Sx1fds2rU7GAgXqm-*+{U!0H zUj1&ko|zwamyMaLcdO?I^)p;vhTzSMli^X@qH~ z1HTuTqhZPG#~eWwa!Wf3wo{q;KWAZDP~ew1Ge3XjB~9c8MUaRuBF%hQGNtUpzNVF3 z&N*&o!IEEhCiYH6yelwrfR#=}YsW(;ZB^|0S`J-;yY7At5wr}T*mH;13-$%}n&B4h~ z+oIo1zi+7N@tpoIHPHBCVUyUM3dz1&jxe=UvB}5Z1U=8i z#DkL56o>vTM@*-cbi!%jZXFgblt=W7pq1IPJZ655XA_AnoGAJ&^~V!!hg$&<8R-?~ zdIYqPi1HASxt`e>|Ac|q7hIhWSTY2ZlknO026hlQkZg_R6Bdj0+=Z?s5=2R{6UIaS zQ%f)rc7WT^$I?@8ImMnD(Um*81>T!}=+a)-_j*15MSN|rDYWE6i>|_zhs~|5PN44x K!a71y&i)H#Gm`cI diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png deleted file mode 100644 index 0a489f8e3c65fd20dc7a10b6bee068e1ec3773f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1635 zcmbVMXH=7C6b@R3AfQDf7KEc6Vj-plK~TU^ECPZwkO7|rQXnZqMq)sXj7nXMa70On zL{UV>mk?GN2{WveBtWf1q9qO>pdSbdCk;y|Q0b5Uv+p@~p69;rea?H&z2@!Zs;~Q* zE(`|KcRTKaTDEg5qNBB}ua5gWEgKCA%JnFW*JJv6IrxNh#PbLYR*|WOOzC2(L3SAOPcPx_O-SKyAj%2F|+F8B*ZE<~<}A^e(Jo5Hxw>98Epe3H&v{l7+w0K$TvhL- zmJW;(%$MK&s1u`8E$i5T z!ruu<@K$Oxh{?J9Hv_H{dO5?DkPuRUv>3XTnl<0^EzB~`pGuLg^Vb7m|jTovS zm5-2n9OC8YNtkGiVm)D-m`~CwuijhY3<|Vc_GWJnftOQxyttQ>L$3bB>#UZ?bxm_M z5Im77Ibm%gKRe4)1;`MBMRvyIc;v4QVu^XbVIXmMYiwje6`0A1s-VWO&_pehp#ta) z2>i4|AhoJW$#{E4hNv_MBxfCx9NtD-j&)o3W+V4euW3nj^3I7;!GlK&WJJ(a5hv>P zzUEVU#)H`)a9|a7wrCCJ{4dNL`~b@b>`Rfx`GDPj^q>d?>{rWYxNGGGzKTD>68Z3N z+BxXjGq(bHnj7DR$Pl+jm=PM^4Fpm>ie{DmEt~dZ)?kZkEzDLz@DGG|YlLWPgrF%9 za9-s-UjV_qha}^KR98;a2HPg7Sz%ob#P}yQ#K{DsHrw)~j#XE}%!&n}eRbB99B3RG z1%czGP0|B4S6<)Y2p;r75-tJ3<0f(h9et2E-6jH;%LJnwss+};IF9Al&t;<34~^@^ zD6+yQLu<-)Na*KiMLr~#oUj3K>Ghl_Kk9gBVY$U_-)OX=b!A>`d~obEOY_nOwRM~H zQ}`Gi1n@h~v36QLjg{%4Yg-OnU54}rr7smpTjR+!s6{i# z{M91+=jIz#vMHpbe&K#CyFMv^w0oG;Q9n~~i(Qp>qsnJ$yv$KX*>=Yyk0Rxt?=(4R ziF_s>!8R%f8V+l^eF7d~8f>3Y0R7qI*E6zzAVoMP~7yOzP{(U)`L zofQSz7wER4@eI?K7RpBHv{2Jy(Iv+epg)}FwV5gJU!CFyd&~uiOg-pbqA!H0${Q}z z6~fu{w_do*4@E=d)l3a8$k<3e~R xW}swA(mxzAO`D=oO!$b8T9^L^j7v$FUxf7$7}(TQi~TD%xE=Fy;T;XS_&0B73PS(@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png deleted file mode 100644 index c9286262f7732c9355831bc456383da331df993f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1874 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 bK8n$s&DnBoX#O%+GsE_6I|J(d!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 42f37898f32702305a043134136730f6a68576ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 959 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2IfRh7srr_TW{~~^@<4;X}Gxh#*(kDYjT?Pp9bH!d85O%D}s~l zq8E!cr{|m_244-1viJy06__I^d{_Lu^qKo7=WR{+zNvqH`ksoyrHq$^tvF-_tQ1Nd zUNl@_T*&Oi-ogjM$b!u<37DWli8cd7DNLl{f+Iq|kMftb&v)%Va(3xat4X)cCtrN> z<6rUO=KgnZ8z9bx2Mn8JL5iB@W`8Ee>QOeg=#x%6GS;||G$6xR`}<p`K%l2c==j)uOtn)$zP_xlR9W$|TQ7gm2%e|F`f&z;Phx=!m4MV`3; zWN&yQw(xxHZ?S7jlV{F4eQb7~)A|FIj~8Avv7Iif{AK4)$;+iG!Pj+nyS*qbd}ucR z=GwYM-tN?sTnhI84=|!e4KTj&L4+$EF%lO_ tQiA#b-CT@7AS%%!C2>6F{@~xsuw;3N{kOk+X9IH}gQu&X%Q~loCIEBFeQp2% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png deleted file mode 100644 index 8996f8113e0e1ff499f0bbab61915326afad17e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1560 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2G(7kE{-7;x8B}8-yNDNao}Td(F_xA-CvB7o`Kzyl4O22u2zVd zd1X?pdJM z{`gKh&yGdg-?mzuKYp|0v+JDr9+rjMHUBbv37V7sc+K(1?$66NK3H14`*fw@x!;A~ zq}iSFZ}(ZOKU!&hF7L6!i`;6Pb6#`Odzcn(e|yei{qf4J#m%e>&GRkKSr@)Dpb^(U7~OmRhGTCbfBEI#U#I->caGmQU-xEy+ZDO0svn0Ef8R9X-?ZS>h0Ffg zy0^|x)8*Uj^5SZx-nqElGiUET>?2rZ`mU?^MBKA?7Yw3X;##La&e=YF=WV+)VhhE; zJIrA&n{>7@@fgS671MX!=66%+ubW}$q_0+MUp>DzW&8Tiu@wu&ODi$iC@?_K6&u=3Z z_ScyDPg|Q*a@zR)v=+NXUjjtpkITi+`YD$z@GD~8wzj023yLKR{SO4YDSR=!H~;?q zpQTBvZ|D8oH0Of*hb5vS-&+JZY`43wU0`87qk;ARiIzN;buTRM&tKj?_1%`HM`e7w z9bQD*r2M(MfqP;4W|BV0f`nFh(yY_kumA{<*eHJ9zt6gdQE*wSt zBviFKNYrHAjk7!ceLH>hby&ciulKym7p8xkD=L!C?sWcLw6?}MzLt4+uZIQ9k>#-c z{rgLa@QYQZBE?(FmHl2Q?@ZxM^Z>ehb^?<$o_+KD$6W%y5_XTZN0Z#)=B@#n$j7jz3p=0GfvO# zuRd0gIm6UR|5?w}8HOOiS@Mtid9zQZC4-{(UCA8jFynIbwwp>{wAQ_P>DZbLzZsIrhfBJZ+#})6#a~ z_LRI`OV_il|82zMlpp*{b4%#`SoasXUi#-RpMNp`b8rU7Uf-7&qn6v605g=Pt>E{b z^gW7S%xXoSuW>h0{?g|9VE$|-NP&SUVlWB~SeesIdSS#Ni*9Z^=c`_Zn2Xdc)*voK iDZ4=FcE*l+hJ#y{h;l9TnguM{89ZJ6T-G@yGywq1*V**| diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png deleted file mode 100644 index 6ca91fea575545c0267439c639284927b468ca6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2229 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 pMgfP#g@^(U~DZV@rU_-DEB`Z~LSKs z_d)zt2B;X8pJT*IaQ2$4ICZAr>anK93IeZyNv+u))@*bX$-v)iA1T)Ji- zCI3_Sko@o7|4#miFpptphs+PD^)$BOo))8l14j_=1}gr?3@nik*t)NQ*g6B>-5byp zt$3%xl!l5AH4`V!8wdCGlUiLPy+}_txS#^IFGYd*y$whavOCY^+dbbbxA*wouQMsQ zBfbG3ATyr4;P|!HyiLskURz|1+1AflwWtH%zgxYRzgJC#LzMY$ zQX^pn)%2zrbGl9}9q+X{S18uKS0|wFq}7_u`pO2w;^-^ulYem21b}^k5E_0JrfLh@ z!$w>xVu#ermgIP=f{e%*d5pVMGvflT8bw4jd;D|z>gCO=(UI5}mb+=Xu$yA@M=3Fs z-F-I5bi|IXUOKY0Y0j788(N=nQwS}ONM(GY8_IC+W-+TeWGb7N*5W4@kp0`QPBFkV zA--7I43Enn>3v0H7gvSG^No`e`Hmr&RX+IyXEG6tYI$3KL3&=w_Z$QH$BT~*Tc1|& zD7-n@v!Z9+zhd#Qd4RDlD!Y~<&R|cA%ZKRJN zQ)$*xLQPt1@chooi=B#~_piA^hedh=|I0$B<-U;oUvgLGmqOmm2ES)z-~{wYrhDx` z-w5W$)Z@lZTbEjthecZJ+1D}(&QmEhD1xduvsM+}FOy`|Ap~S;)fcXzh2-@#5PLb? z>pE}B#z}Y^=M|jU>Q!Zx+5!i%qq4qm9;L!Jy3`KhJb5;Vv=idC#Pn1yd~F$SaJVzI-0_BP+Fe2V8cd`w@$0n$ocY z-USA-6Awe3M;~72nJ#b+UMaKELyOxcmOkfw%H@$w9S@5L$lHP3uVfQAX&W@7v1*w% ze%rYi>OKR@+L_=|E4N{nCt-m{=KR<8x}mr?;77PC z^NeISjd`h>ig+*^=4qL1wtxlR;{{9R&E#~2-$Yrujs+cbKRsXG3QCTv2FfoEZ zkt#LSq~|=_+_XVxP&}%{_Re%4wUJBgY@;oDDERifSqD;_N(%i#ygh36u$qud7p`~+ zSY3-+(C9qnu_<0ANPev<%C^6gHWxgcIKCi#D!Xt6=s0zMmlERBHluSuvIh$AfH%9y$<4r^>S&>-1gk`k->dm3b1IoQ#qd8Kw z?C@}4E>x8nD}czwwN(k98_(mwNAVT~jn6>G1x29?9JtbB8hd#cGD%;RE$bZPCZtW{ zkBKDrv$+%Ti<6Dn(c>nhINh{Lru%fi<>ECKB#&ICoOYEmxFcfueaf@)xj6N!B{)oX z6+aTxBC64HAzV!o(e&wESX-F~M+ZMu_zafD8MAK0%xd;4NbMIT+J{)@Vu0%CgpzdHMeO#fxc21%Mt7sYsc>Lx{?_R|44 N!lvI6;>1s;*b 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 X(2HI4rOY+SQ*u6j>R6S7|K;BRK`9M# diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png deleted file mode 100644 index 80cfabaed0dcbe3f9d8b99dd2165cb787f50c92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TrBr5} zn3mUI4b(*hI3Rvq_vKp2T)&`M7oVvt9U2TKB0M(WN`4%-zopr05vgxLI3~& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png deleted file mode 100644 index cd97328f72f122ff4802795b30abc04a94fd14bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 950 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 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 9fdbbc1761fad331d63e854038f35474838fe5ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1203 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 HB{Ts5!l2Ei diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index 8996f8113e0e1ff499f0bbab61915326afad17e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1560 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2G(7kE{-7;x8B}8-yNDNao}Td(F_xA-CvB7o`Kzyl4O22u2zVd zd1X?pdJM z{`gKh&yGdg-?mzuKYp|0v+JDr9+rjMHUBbv37V7sc+K(1?$66NK3H14`*fw@x!;A~ zq}iSFZ}(ZOKU!&hF7L6!i`;6Pb6#`Odzcn(e|yei{qf4J#m%e>&GRkKSr@)Dpb^(U7~OmRhGTCbfBEI#U#I->caGmQU-xEy+ZDO0svn0Ef8R9X-?ZS>h0Ffg zy0^|x)8*Uj^5SZx-nqElGiUET>?2rZ`mU?^MBKA?7Yw3X;##La&e=YF=WV+)VhhE; zJIrA&n{>7@@fgS671MX!=66%+ubW}$q_0+MUp>DzW&8Tiu@wu&ODi$iC@?_K6&u=3Z z_ScyDPg|Q*a@zR)v=+NXUjjtpkITi+`YD$z@GD~8wzj023yLKR{SO4YDSR=!H~;?q zpQTBvZ|D8oH0Of*hb5vS-&+JZY`43wU0`87qk;ARiIzN;buTRM&tKj?_1%`HM`e7w z9bQD*r2M(MfqP;4W|BV0f`nFh(yY_kumA{<*eHJ9zt6gdQE*wSt zBviFKNYrHAjk7!ceLH>hby&ciulKym7p8xkD=L!C?sWcLw6?}MzLt4+uZIQ9k>#-c z{rgLa@QYQZBE?(FmHl2Q?@ZxM^Z>ehb^?<$o_+KD$6W%y5_XTZN0Z#)=B@#n$j7jz3p=0GfvO# zuRd0gIm6UR|5?w}8HOOiS@Mtid9zQZC4-{(UCA8jFynIbwwp>{wAQ_P>DZbLzZsIrhfBJZ+#})6#a~ z_LRI`OV_il|82zMlpp*{b4%#`SoasXUi#-RpMNp`b8rU7Uf-7&qn6v605g=Pt>E{b z^gW7S%xXoSuW>h0{?g|9VE$|-NP&SUVlWB~SeesIdSS#Ni*9Z^=c`_Zn2Xdc)*voK iDZ4=FcE*l+hJ#y{h;l9TnguM{89ZJ6T-G@yGywq1*V**| diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png deleted file mode 100644 index 6ca91fea575545c0267439c639284927b468ca6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2229 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 pMgfP#g@^(U~DZV@rU_-DEB`Z~LSKs z_d)zt2B;X8pJT*IaQ2$4ICZAr>anK93IeZyNv+u))@*bX$-v)iA1T)Ji- zCI3_Sko@o7|4#miFpptphs+PD^)$BOo))8l14j_=1}gr?3@nik*t)NQ*g6B>-5byp zt$3%xl!l5AH4`V!8wdCGlUiLPy+}_txS#^IFGYd*y$whavOCY^+dbbbxA*wouQMsQ zBfbG3ATyr4;P|!HyiLskURz|1+1AflwWtH%zgxYRzgJC#LzMY$ zQX^pn)%2zrbGl9}9q+X{S18uKS0|wFq}7_u`pO2w;^-^ulYem21b}^k5E_0JrfLh@ z!$w>xVu#ermgIP=f{e%*d5pVMGvflT8bw4jd;D|z>gCO=(UI5}mb+=Xu$yA@M=3Fs z-F-I5bi|IXUOKY0Y0j788(N=nQwS}ONM(GY8_IC+W-+TeWGb7N*5W4@kp0`QPBFkV zA--7I43Enn>3v0H7gvSG^No`e`Hmr&RX+IyXEG6tYI$3KL3&=w_Z$QH$BT~*Tc1|& zD7-n@v!Z9+zhd#Qd4RDlD!Y~<&R|cA%ZKRJN zQ)$*xLQPt1@chooi=B#~_piA^hedh=|I0$B<-U;oUvgLGmqOmm2ES)z-~{wYrhDx` z-w5W$)Z@lZTbEjthecZJ+1D}(&QmEhD1xduvsM+}FOy`|Ap~S;)fcXzh2-@#5PLb? z>pE}B#z}Y^=M|jU>Q!Zx+5!i%qq4qm9;L!Jy3`KhJb5;Vv=idC#Pn1yd~F$SaJVzI-0_BP+Fe2V8cd`w@$0n$ocY z-USA-6Awe3M;~72nJ#b+UMaKELyOxcmOkfw%H@$w9S@5L$lHP3uVfQAX&W@7v1*w% ze%rYi>OKR@+L_=|E4N{nCt-m{=KR<8x}mr?;77PC z^NeISjd`h>ig+*^=4qL1wtxlR;{{9R&E#~2-$Yrujs+cbKRsXG3QCTv2FfoEZ zkt#LSq~|=_+_XVxP&}%{_Re%4wUJBgY@;oDDERifSqD;_N(%i#ygh36u$qud7p`~+ zSY3-+(C9qnu_<0ANPev<%C^6gHWxgcIKCi#D!Xt6=s0zMmlERBHluSuvIh$AfH%9y$<4r^>S&>-1gk`k->dm3b1IoQ#qd8Kw z?C@}4E>x8nD}czwwN(k98_(mwNAVT~jn6>G1x29?9JtbB8hd#cGD%;RE$bZPCZtW{ zkBKDrv$+%Ti<6Dn(c>nhINh{Lru%fi<>ECKB#&ICoOYEmxFcfueaf@)xj6N!B{)oX z6+aTxBC64HAzV!o(e&wESX-F~M+ZMu_zafD8MAK0%xd;4NbMIT+J{)@Vu0%CgpzdHMeO#fxc21%Mt7sYsc>Lx{?_R|44 NJKUoMfFLne}|@Sf-TqG#7le%)BsycZCB zdpqX=YI3|eY4SxHAi)xl;OXR}>Z!MXn_eHey773GeZv*!rkfA!LLKD)!dzE< zO(|-B2LHS8Kk(1&-@!jj98?D@M(&@B-Leo%Akyak^%rCR@uK$jEh&td*05@thhMdDRe&}i{ek99_i{T(1+*O_LFVE^DQ%5 z#8d3vnNFc_VkS+|;VdbPe?O6-xJ5d$NTS3e-i;NzrWB3ts760O&WbW)7w^E67#~D6 znC@VjC6(qu#mX98tWF|qJ0ji@t}_io+~+{2&$>RM;bOTBuBE+_q&G#9YI&<&uX8!a zLQhsC3=XnVM;AR&6r5>&eV##2!mPm&4;HLGs=2bUpIGS~ zs*fku;mj3-oYJ1m7EsW1%u~z7+~d@;dAtqqalJI91G1-~#Zq0&qThlck5%>%D)wM8 zn`Lby3-h$3$HUz#uk*L88a5#8KX;4o&h=zpc&LFBJrub#SzGJsyrMZ1N-tZxE|hLr zjd)~d-G@)frM)!T2I2W6f%aX@>j-m?!tV+i-k{sr2q7{G>C(8w+1$5;rE|G*EJPLBoCqQ6;WGC-`Uk?!<;V)R5w z@2Znx;1W))jiarR)cf}w{3@##=1MX_S0 zGiI1cpAy#LA_GBuN>xPD+cAPbKsMAD3RfQsa&6>k<&}&mK6W+&Q^j3 zBSF;Xy??rS#x}o|a4A3!6Lr4_CFMS$dWPKRsqpy*$wf zEEtA$+(r3>vs=3~81qO{!`Vwynade!LH&saK>rp=(`xwW`rs#Jq_eF@QIiBmiXubjA(7-W}aE%?#(8N(g$36~-EjcM@aI z;nEbQtH}AwZIovkeknLqRnWzBv<|a>wvpkNWPGg=_R))XUVF6isV)PmU8a1#@+_?c z@ci4KWpUDv^k5WcVF8}YY>R=g?2RE~XUZawsoW6;h9O?NnSdJHW0#wx!GJVv&uEN! z;^lxXQTuc;$Yri90-nls3^KXl{1q*u^=BAn*G54rBBD~c$lT)i@mK@S;_8>EnHv#o z*9S#9Wr}VPTvE1SI^DJ!W*NLpkvbpZXOEM{tWj>;JUpWTubeO6ncnCNM#Ro#^k+b_ z`8VAUbST5}Vddb+_JA3&up&$u#ZwNH5Ik$OHac#i2Grhas6h4~V5(ym#riql(zxZe zZGDdoMEWiffI23`_T3l+cg z8+%^ZYfNYIfH0mcI}*<)gHk17AEvY~@Ix3T_!Sq&nxK0upjit^pqYYNSW!(qu@)LE%)H1Rup zEE)*M*|dDW*Wn5YE0USWVRX$+)hEgMzscCj+)@|)Hf@S8kgr8)&W6F=MFEzZW$Mau z5!YUCDo-gc*t%72a2W@gux4~`2S;4jhZlskzW^aTPK^U4F&o>fpVQt$;U|WGk2qob z8t%A|>+mTJx^DF5kU(NU&T4%ewqOBP_9-BT;y)U1!yY^@7h$2HH{E6WVeJ+zlP}M) zPYhvnt-F_t;(x`dGlkSBVfv$Z{}T~x-Q^YyMv!jIY-hqHcJj%6mn=$c>pSaptm<>W zH5_sfAIaTK8f{cCJ_!mQI$ywPFa$p(Us{Fg^KC pcELkcA3*-zX5s&Mi|mo>I#vl(-D=J&nMyVn2! diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 6a173a4d09d91f798b792439a20da718dbcbadc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2158 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 bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png deleted file mode 100644 index 8939c7b505939631f6bfd7a3d9147ed137dd1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2182 zcma);dsNbC8poZP%G)M+X&5s@vocK~HEq0-c*`X(yS9@$6dg5FK}t!pNT(@< zT+>6TT}%m03=OR;v-l}Anu-^kG67SEkspdE#%i5u+MToK%pcGDocH@a=X;*#ocBGS zw=gs~V7=ZZJuNM*^@00+|GttJS3_sr%6fJ>!gD39!FmUIYiY5H_0=(JSN6Ke`@>VU zvwWO#(9Gy)?NN;B!`Dr{_gsSFdT~a3Tp|kla&9J8g&JPV z)q53dxbe21;TezOu8koK$K zgZn2FiUG>HI`FTUXWeXiox{I*$Il5v;}d4&vTRQ=3g9?0pOo8xvA|@_vZsaObVnOJ z=|t!BS&oA^f1_H4p$+2^phV-D6#V4DhE$KoQTN{Ia4YYFFBj+O5Smp6Eompnl4nS) zmRa=cK}+}zizeeekXGa-aCXa^w(`1TCumYK3_LLIRVWxRIq6j?tGf5ZBYw8p%4;3kQr_D|E2pYb0Q4^}sBpzuH7NzW+Si68iR?=U zLn8G2jkA1n!bXU@#O7?7rlMK1r^$LMn(c!KkTahhJvIe5UE-SyuS^PfsdBCnoC8yW zY%Hz&PY0>3=sr}<6 z|L@8!JNrxXp2K4AMx))De;_(i1=@zRarIcxh4VuCG4G6AtxN4E6M@#j6 z=3-A327rtsvi|X9k(AV8#p#Nvtlx<*5-eqJz9N?B)jJEFXJa;?WPfYJQTfoRs7P?8 z)LTm8ymI-A*adBo#wZ@MFWo04c_7Ida@Sa&q0QNMpE~Mr@vw%qdBX|16j75oJ34tj z+Tc!xwQ% z!am4}gk)PPdX6`!U)_bGWPo#N`=)u6^r|pgRU3n#c)e&h%Z|zW@NRKVZ{5e?hd)}0 z%Re}@xYGoNi|@FxtJVHVU6)d@c4X)2GlEeLrAUC`TW9rmMCx#zZrHD=PrKHiaI8;& z!7G~9=D81_l)@E7SMRRPYt%-`M)X&Q*eAW}o}|AH3!#Wqpn_;5JvO|XPBXK9LlIqy zV?P>ldM|(;&uhY}8Inuh=h@dX&Oa~wmZ4nmAQ|l~Nc58#(3%3I+)Ov|wHaym(+05) zci?)K7nsv`LLvUw_$vbj6`MJjB@Q%jzXlsSd6$7usc>%l{H{dd@FEuQfxiaq@+%S%EM{{e zVG>oSuBwhcJeHqs}mq1uDMhav%FuUBbC z{#C>n-y3-*U!ciO)A#~2x473{I3=)=H!Y$f@a^aQEfnZs6L5mw8kX(-1dFv>Ao`7b zRX>|Aph-L(ls8=HV>R=~)9WDQffB2QF!*z-00UEFV!dcX#a1sWm4|o*?vc>OUE-da zWoI4I1(7Lui#~d5dCyDaQ?=`y#?VhSb$`3KpKd;hMCL$Hl{K6Dmv|$_sczzvg)99C zZ8W5TCnJZiiA)>2M@)Fq#{Gd>zd_IYPtcGW5vTjm@DDWm1knFu{y$XxQ>ZO) bRT5fa-6{2<<-sc-39UfCVBdOFbnbru2yQ?v diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png deleted file mode 100644 index ade8cd764ecab6b1a5137132e7a5b5731f39063e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 697 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bv6F7srr_TW_xhayA6;I9xQZNIkR2l_!CVIpRp&=7#u3#}l5b zNJvjV8yNGhywB~?t)v6zmx(r9U|h)T#NNWkAuC{|P~z|cgi!=x5^zCe)lflHBN{F^ zq8g!40x=tI1jtN+<~GBeHihHwnOn;1&iV!YmAfnRa51Uw-OhwzJ;J|WH@#mdA6R`v zwR-N7U$!4EB83QsBROQzv^ud9^gJyB^@qwDh8mKi3Lc~ge-VzVS=9^~|9H=f+uF?m PracBvS3j3^P6!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png deleted file mode 100644 index 83c95a63847d06f8c2dc8ddfb063adf51894b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%xMcgnxo^DnqAjQB#**w{HwJY3n-LxcS@XcL@85w} z>AqhT-MzCryTL&~RX@Okmx!=Ia~mjjFv48G>VtD8%Y#MGNP`6x7IkQGgAu)$af4yM b!A|MdllUG?GQG?LOnnTVu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png deleted file mode 100644 index a46206b47405039019d8d01ed680f87c07d16fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%P$P<^A!W7WR(DQ)R@+iZ66`ePJ z2QPhH>dmw6SES)Yn<%zDIfhIERthB!FB&c|E@XCMZ{g#R1z{9Hm;_AF5m_}v5X}ey zD>Nfo_#kG(i~w6k&|H}6#`md5;*#w@&U)h=7Hv62H#QyhMZzn%h9JgAwKeRv(-*SspBcMj9-ru&6_e8;t11j2jI5 b4R%U*81g-k!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 9aa2cb468bcbe42e91d924aafb53b4590b9ced61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BtVq7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%IA!|{Nps&g>qT2m(Tye9#cmAf4roUB7-Y`_$G(3D zUZwkfRdn~x?(7DK0aZN#4__hz2hDAu=)nkd0jm$rnJf<$L1PUTR#?=bMGi*%Vnz;z c{RTUwwPx}^(EDd408D-ip00i_>zopr0DYm?wEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d064ac9a034be3efd90840d123f990d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_yA@-{f|FdWQon)$g&kYx@RO9D&%Q3m}(i#^jt zOmfZcx)oO5v6Sk}?q{^Wy@sKMk3&|#N}N2$O&bqNs)lq8ZV` zhiU}l!e)rsFe5;g5jGd5dV$M}^lNg3X4alxt4lT?awpMwEqSa+-oxtPd(QEJ)kkLT z^$Yqd_XF9#Feku6hX}7>Sf)^d5o8$VqK7(`AVV>?MktKI4msLDCK5CiBg`FMpqL7Z eM~t{UYin?Vcdg)>gPFjj$KdJe=d#Wzp$Pz^`_}jX diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 4e721e269e394723f4979ac7f8bc99003b177cb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 695 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bt_)7srr_TW_yA@-`UoI9xP!yw{_Upe4{S@uxPMMN!6!YOZNz z_O@7cezy*<2Lj_TdU|iUY zYJ|fJh}m!>KxPs)7v>5L!CzCi&fhU>?UG-1yCNShLUA#M$q28&9B0l%$gcMb*RS|{ zWbWQDqr=;exPrq2!bP0l+XkKJxSH` diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^1}0Ze7srr_TW_x(5a*g+Us&=Dp96BO7b2vrIbad?5GA0mimXbT^zp+wpQ zbyuUz!tXPGHhx~a|JMYG`D=Zawsxbqx*6hfe9pPc0rwzG8H%r4_;OkV8&HiUDlm|P zj!3U#IJe;fW+0=R3v)G+H_(F=!(5D5Q7A$4GzY%;FSx^`mdo)>POf|lFqJWQy85}S Ib4q9e00!gG3jhEB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^1}0Ze7srr_TW_xh@*Ys&aX4t^DDZ4q(zAjqk`JaYNi65`cewQBJ@K9(G2B~MKzRI zo1pHpb*f+enOXbUdOMZmo2SC&m0W~6mRQRcN;4uHi`Aozy(UZrsKz?HKnWO-0R#*I qn@oDx5MeGxEYLj6o!I++Gnj_5KMVT&ND7$B7(8A5T-G@yGywqFmCcC& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 5f9ec76a75423d4265fd9fe6ae5c01cb4afded4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|~;O7srr_TW_xhaxy6JI5@ITGhCp_lOdz~V7mG@itiA|0aRld$q5=-2LzHg2nH}lFbG&- jMkPu}qIh{n9`mHPY|oBuTJr&z!WcYV{an^LB{Ts5kYdVC diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index 5ffb0e92c7da21ad7a59cf0262f5a49dec670709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|~;O7srr_TW_xhaxy6JI5@JiZJML0ldi1$V7!lvI6;>1s;*b z3=DjSL74G){)!X^1||;f)vACj93w{LXK*1fFVaUTC7;)F|ze?JX`0^I}e!97(8A5 KT-G@yGywpT1!lvI6;>1s;*b z3=DjSL74G){)!X^1|~aC7srr_TW_yk-s+UC4$&%`pneFi2k22WQ%mvv4FO#m!D B%)9^q diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png deleted file mode 100644 index 9d5057939..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png deleted file mode 100644 index 17360edc9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index ade8cd764ecab6b1a5137132e7a5b5731f39063e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 697 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bv6F7srr_TW_xhayA6;I9xQZNIkR2l_!CVIpRp&=7#u3#}l5b zNJvjV8yNGhywB~?t)v6zmx(r9U|h)T#NNWkAuC{|P~z|cgi!=x5^zCe)lflHBN{F^ zq8g!40x=tI1jtN+<~GBeHihHwnOn;1&iV!YmAfnRa51Uw-OhwzJ;J|WH@#mdA6R`v zwR-N7U$!4EB83QsBROQzv^ud9^gJyB^@qwDh8mKi3Lc~ge-VzVS=9^~|9H=f+uF?m PracBvS3j3^P6!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 9aa2cb468bcbe42e91d924aafb53b4590b9ced61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BtVq7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%IA!|{Nps&g>qT2m(Tye9#cmAf4roUB7-Y`_$G(3D zUZwkfRdn~x?(7DK0aZN#4__hz2hDAu=)nkd0jm$rnJf<$L1PUTR#?=bMGi*%Vnz;z c{RTUwwPx}^(EDd408D-ip00i_>zopr0DYm?wEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index a46206b47405039019d8d01ed680f87c07d16fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%P$P<^A!W7WR(DQ)R@+iZ66`ePJ z2QPhH>dmw6SES)Yn<%zDIfhIERthB!FB&c|E@XCMZ{g#R1z{9Hm;_AF5m_}v5X}ey zD>Nfo_#kG(i~w6k&|H}6#`md5;*#w@&U)h=7Hv62H#QyhMZzn%h9JgAwKeRv(-*SspBcMj9-ru&6_e8;t11j2jI5 b4R%U*81g-k!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 83c95a63847d06f8c2dc8ddfb063adf51894b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%xMcgnxo^DnqAjQB#**w{HwJY3n-LxcS@XcL@85w} z>AqhT-MzCryTL&~RX@Okmx!=Ia~mjjFv48G>VtD8%Y#MGNP`6x7IkQGgAu)$af4yM b!A|MdllUG?GQG?LOnnTVu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_yA@-{f|FdWQon)$g&kYx@RO9D&%Q3m}(i#^jt zOmfZcx)oO5v6Sk}?q{^Wy@sKMk3&|#N}N2$O&bqNs)lq8ZV` zhiU}l!e)rsFe5;g5jGd5dV$M}^lNg3X4alxt4lT?awpMwEqSa+-oxtPd(QEJ)kkLT z^$Yqd_XF9#Feku6hX}7>Sf)^d5o8$VqK7(`AVV>?MktKI4msLDCK5CiBg`FMpqL7Z eM~t{UYin?Vcdg)>gPFjj$KdJe=d#Wzp$Pz^`_}jX diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index 0843d42fd77db6ec61d2ad51997da70be8dd3a97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_xh@-`UoI2<&aY15;?AS&D-_(znvX3~WX)~w#w zr}t&7`?fksu`*Bc!=^+Qg%XDs4Hp;}GCQ%i@NvirSb;E#AWQ-#*o>?iBIt-}ghC0L z5gf7*vtdSnEhA_yT=o6a9g%zISLl3Gy&U%K*8Y-!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png deleted file mode 100644 index ade8cd764ecab6b1a5137132e7a5b5731f39063e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 697 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bv6F7srr_TW_xhayA6;I9xQZNIkR2l_!CVIpRp&=7#u3#}l5b zNJvjV8yNGhywB~?t)v6zmx(r9U|h)T#NNWkAuC{|P~z|cgi!=x5^zCe)lflHBN{F^ zq8g!40x=tI1jtN+<~GBeHihHwnOn;1&iV!YmAfnRa51Uw-OhwzJ;J|WH@#mdA6R`v zwR-N7U$!4EB83QsBROQzv^ud9^gJyB^@qwDh8mKi3Lc~ge-VzVS=9^~|9H=f+uF?m PracBvS3j3^P6!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png deleted file mode 100644 index 83c95a63847d06f8c2dc8ddfb063adf51894b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%xMcgnxo^DnqAjQB#**w{HwJY3n-LxcS@XcL@85w} z>AqhT-MzCryTL&~RX@Okmx!=Ia~mjjFv48G>VtD8%Y#MGNP`6x7IkQGgAu)$af4yM b!A|MdllUG?GQG?LOnnTVu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png deleted file mode 100644 index a46206b47405039019d8d01ed680f87c07d16fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%P$P<^A!W7WR(DQ)R@+iZ66`ePJ z2QPhH>dmw6SES)Yn<%zDIfhIERthB!FB&c|E@XCMZ{g#R1z{9Hm;_AF5m_}v5X}ey zD>Nfo_#kG(i~w6k&|H}6#`md5;*#w@&U)h=7Hv62H#QyhMZzn%h9JgAwKeRv(-*SspBcMj9-ru&6_e8;t11j2jI5 b4R%U*81g-k!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png deleted file mode 100644 index 9aa2cb468bcbe42e91d924aafb53b4590b9ced61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BtVq7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%IA!|{Nps&g>qT2m(Tye9#cmAf4roUB7-Y`_$G(3D zUZwkfRdn~x?(7DK0aZN#4__hz2hDAu=)nkd0jm$rnJf<$L1PUTR#?=bMGi*%Vnz;z c{RTUwwPx}^(EDd408D-ip00i_>zopr0DYm?wEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png deleted file mode 100644 index 1fed1d6d064ac9a034be3efd90840d123f990d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_yA@-{f|FdWQon)$g&kYx@RO9D&%Q3m}(i#^jt zOmfZcx)oO5v6Sk}?q{^Wy@sKMk3&|#N}N2$O&bqNs)lq8ZV` zhiU}l!e)rsFe5;g5jGd5dV$M}^lNg3X4alxt4lT?awpMwEqSa+-oxtPd(QEJ)kkLT z^$Yqd_XF9#Feku6hX}7>Sf)^d5o8$VqK7(`AVV>?MktKI4msLDCK5CiBg`FMpqL7Z eM~t{UYin?Vcdg)>gPFjj$KdJe=d#Wzp$Pz^`_}jX diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png deleted file mode 100644 index 4e721e269e394723f4979ac7f8bc99003b177cb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 695 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bt_)7srr_TW_yA@-`UoI9xP!yw{_Upe4{S@uxPMMN!6!YOZNz z_O@7cezy*<2Lj_TdU|iUY zYJ|fJh}m!>KxPs)7v>5L!CzCi&fhU>?UG-1yCNShLUA#M$q28&9B0l%$gcMb*RS|{ zWbWQDqr=;exPrq2!bP0l+XkKJxSH` diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 8071681d6c3891569c414db8f91e771d75a4b5f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B>UXA1RM$3jzEqqSw zEqolZ0#*to4lf!mFfMEcVrFDPm;_vq<1Pn8DO^OM1fd@)h-xTM5Y3V8jA&vVDBKiahD*-S|kyN7f7C)$F0mDjT8`Ib;toh xoSSJK7D!$o7{(YufZ}0@cI3E6^YVk;+%4DGpT$TRyay&Q22WQ%mvv4FO#sGw&B6cx diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png deleted file mode 100644 index ade8cd764ecab6b1a5137132e7a5b5731f39063e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 697 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bv6F7srr_TW_xhayA6;I9xQZNIkR2l_!CVIpRp&=7#u3#}l5b zNJvjV8yNGhywB~?t)v6zmx(r9U|h)T#NNWkAuC{|P~z|cgi!=x5^zCe)lflHBN{F^ zq8g!40x=tI1jtN+<~GBeHihHwnOn;1&iV!YmAfnRa51Uw-OhwzJ;J|WH@#mdA6R`v zwR-N7U$!4EB83QsBROQzv^ud9^gJyB^@qwDh8mKi3Lc~ge-VzVS=9^~|9H=f+uF?m PracBvS3j3^P6!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png deleted file mode 100644 index 83c95a63847d06f8c2dc8ddfb063adf51894b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%xMcgnxo^DnqAjQB#**w{HwJY3n-LxcS@XcL@85w} z>AqhT-MzCryTL&~RX@Okmx!=Ia~mjjFv48G>VtD8%Y#MGNP`6x7IkQGgAu)$af4yM b!A|MdllUG?GQG?LOnnTVu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png deleted file mode 100644 index a46206b47405039019d8d01ed680f87c07d16fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%P$P<^A!W7WR(DQ)R@+iZ66`ePJ z2QPhH>dmw6SES)Yn<%zDIfhIERthB!FB&c|E@XCMZ{g#R1z{9Hm;_AF5m_}v5X}ey zD>Nfo_#kG(i~w6k&|H}6#`md5;*#w@&U)h=7Hv62H#QyhMZzn%h9JgAwKeRv(-*SspBcMj9-ru&6_e8;t11j2jI5 b4R%U*81g-k!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 9aa2cb468bcbe42e91d924aafb53b4590b9ced61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BtVq7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%IA!|{Nps&g>qT2m(Tye9#cmAf4roUB7-Y`_$G(3D zUZwkfRdn~x?(7DK0aZN#4__hz2hDAu=)nkd0jm$rnJf<$L1PUTR#?=bMGi*%Vnz;z c{RTUwwPx}^(EDd408D-ip00i_>zopr0DYm?wEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d064ac9a034be3efd90840d123f990d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_yA@-{f|FdWQon)$g&kYx@RO9D&%Q3m}(i#^jt zOmfZcx)oO5v6Sk}?q{^Wy@sKMk3&|#N}N2$O&bqNs)lq8ZV` zhiU}l!e)rsFe5;g5jGd5dV$M}^lNg3X4alxt4lT?awpMwEqSa+-oxtPd(QEJ)kkLT z^$Yqd_XF9#Feku6hX}7>Sf)^d5o8$VqK7(`AVV>?MktKI4msLDCK5CiBg`FMpqL7Z eM~t{UYin?Vcdg)>gPFjj$KdJe=d#Wzp$Pz^`_}jX diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 4e721e269e394723f4979ac7f8bc99003b177cb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 695 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bt_)7srr_TW_yA@-`UoI9xP!yw{_Upe4{S@uxPMMN!6!YOZNz z_O@7cezy*<2Lj_TdU|iUY zYJ|fJh}m!>KxPs)7v>5L!CzCi&fhU>?UG-1yCNShLUA#M$q28&9B0l%$gcMb*RS|{ zWbWQDqr=;exPrq2!bP0l+XkKJxSH` diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^1}0Ze7srr_TW_x(5a*g+Us&=Dp96BO7b2vrIbad?5GA0mimXbT^zp+wpQ zbyuUz!tXPGHhx~a|JMYG`D=Zawsxbqx*6hfe9pPc0rwzG8H%r4_;OkV8&HiUDlm|P zj!3U#IJe;fW+0=R3v)G+H_(F=!(5D5Q7A$4GzY%;FSx^`mdo)>POf|lFqJWQy85}S Ib4q9e00!gG3jhEB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index b11a2e63d99ec75c15fd9bb66796aa5058dab979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4GTS8978JN-k#sc+u*>!yz&10g$q5TRkXTJ zOv`Jq2I`_7_|X4)+LvoRX6l+}Gm0l&8X62n>iM4xlRg}^!lvI6;>1s;*b z3=DjSL74G){)!X^1}0Ze7srr_TW_xh@*Ys&aX4t^DDZ4q(zAjqk`JaYNi65`cewQBJ@K9(G2B~MKzRI zo1pHpb*f+enOXbUdOMZmo2SC&m0W~6mRQRcN;4uHi`Aozy(UZrsKz?HKnWO-0R#*I qn@oDx5MeGxEYLj6o!I++Gnj_5KMVT&ND7$B7(8A5T-G@yGywqFmCcC& diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 5f9ec76a75423d4265fd9fe6ae5c01cb4afded4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|~;O7srr_TW_xhaxy6JI5@ITGhCp_lOdz~V7mG@itiA|0aRld$q5=-2LzHg2nH}lFbG&- jMkPu}qIh{n9`mHPY|oBuTJr&z!WcYV{an^LB{Ts5kYdVC diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index 5ffb0e92c7da21ad7a59cf0262f5a49dec670709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|~;O7srr_TW_xhaxy6JI5@JiZJML0ldi1$V7!lvI6;>1s;*b z3=DjSL74G){)!X^1||;f)vACj93w{LXK*1fFVaUTC7;)F|ze?JX`0^I}e!97(8A5 KT-G@yGywpT1!lvI6;>1s;*b z3=DjSL74G){)!X^1|~aC7srr_TW_yk-s+UC4$&%`pneFi2k22WQ%mvv4FO#m!D B%)9^q diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png deleted file mode 100644 index ff14eccb9522421d5c53b1cc3dfb808a14eb92d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||pZ2(mN!h< zx5ehb*A_k}_7*-4Sph4B5{DNJ7Z?{d12HqQAWQ--$Z?kgq7*KoP=e486+|@@D2Qq( zkv2iy<?OA0}MH;(PG6SkMRK?$Fs+CHs$~m8iS{+ KpUXO@geCw!575p4 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png deleted file mode 100644 index 12e1335aa1d9a4adeb834e67db72f1cd28506758..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 698 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2Bv6F7srr_TW_xhayA6;I9xQZNIkR2l_!CVIpRp&=7#u3#}l5b zNJvjV8yNGhywB~?t)v6zmx(r9U|h)T#NNWkAuC{|P~z|cgi!=x5^zCe)lflHBN{F^ zq8g!40x=tI1jtN+<~GBeHihHwnOn;1&iV!YmAfnRa51Uw-OhwzJ;J|WH@#mdA6R`v zwR-N7U$!4EB83QsBROQzv^ud9^gJyB^@qwDh8mKi3Lc~ge-VzVS=9^~|9H=f+uF?m PracBvS3j3^P6!lvI6;>1s;*b z3=DjSL74G){)!X^2Br{C7srr_TW_xh@-`UoxL!1yY18Awpe10S{6m}VL4Ql@+{Fzx zWtYV!udWNzQ8_-1Q9oOaVIi{zopr071gkg#Z8m diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 9aa2cb468bcbe42e91d924aafb53b4590b9ced61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BtVq7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%IA!|{Nps&g>qT2m(Tye9#cmAf4roUB7-Y`_$G(3D zUZwkfRdn~x?(7DK0aZN#4__hz2hDAu=)nkd0jm$rnJf<$L1PUTR#?=bMGi*%Vnz;z c{RTUwwPx}^(EDd408D-ip00i_>zopr0DYm?wEzGB diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index a46206b47405039019d8d01ed680f87c07d16fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%P$P<^A!W7WR(DQ)R@+iZ66`ePJ z2QPhH>dmw6SES)Yn<%zDIfhIERthB!FB&c|E@XCMZ{g#R1z{9Hm;_AF5m_}v5X}ey zD>Nfo_#kG(i~w6k&|H}6#`md5;*#w@&U)h=7Hv62H#QyhMZzn%h9JgAwKeRv(-*SspBcMj9-ru&6_e8;t11j2jI5 b4R%U*81g-k!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 83c95a63847d06f8c2dc8ddfb063adf51894b5ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bug~7srr_TW_xh@-`UoI2<%{6mUvQVG3wu=uu#CKguv)Mdyv* z!AoD4dh@LN6=^upCW>uOjvcBfypsG#942@qOx%xMcgnxo^DnqAjQB#**w{HwJY3n-LxcS@XcL@85w} z>AqhT-MzCryTL&~RX@Okmx!=Ia~mjjFv48G>VtD8%Y#MGNP`6x7IkQGgAu)$af4yM b!A|MdllUG?GQG?LOnnTVu6{1-oD!M!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_yA@-{f|FdWQon)$g&kYx@RO9D&%Q3m}(i#^jt zOmfZcx)oO5v6Sk}?q{^Wy@sKMk3&|#N}N2$O&bqNs)lq8ZV` zhiU}l!e)rsFe5;g5jGd5dV$M}^lNg3X4alxt4lT?awpMwEqSa+-oxtPd(QEJ)kkLT z^$Yqd_XF9#Feku6hX}7>Sf)^d5o8$VqK7(`AVV>?MktKI4msLDCK5CiBg`FMpqL7Z eM~t{UYin?Vcdg)>gPFjj$KdJe=d#Wzp$Pz^`_}jX diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index 0843d42fd77db6ec61d2ad51997da70be8dd3a97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2Bs)a7srr_TW_xh@-`UoI2<&aY15;?AS&D-_(znvX3~WX)~w#w zr}t&7`?fksu`*Bc!=^+Qg%XDs4Hp;}GCQ%i@NvirSb;E#AWQ-#*o>?iBIt-}ghC0L z5gf7*vtdSnEhA_yT=o6a9g%zISLl3Gy&U%K*8Y-!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png deleted file mode 100644 index 8db71d319eeaeb132e55fb7867372e91d9133b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BuI?7srr_TW_yA@-`UoI9xQFY15;Spe4{S@uxPMg&?2rw|Py= z&icEXgg;+(!{eSi+n<~WMh;m4D}@q=7Y!E}7cx7sxA1{5vS2ez0w#!}8X|~h1cxlD z5kPYxX2XmCSw`4gm}-aa3+p$v`Q2GOeWo4BZq;T$x7rcmSC9=KRE#(wBCa>=WN&BX W<2UhqbPSm47(8A5T-G@yGywqMC)arZ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png deleted file mode 100644 index 12e1335aa1d9a4adeb834e67db72f1cd28506758..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 698 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G!lvI6;>1s;*b z3=DjSL74G){)!X^2BsKK7srr_TW_y1@-`UoFdQ_SY15;?Aj)xo<6l>&s?O`VO-pYt zm)^L;>VB8oCrhJ*O^GZDB@QndE-)@+c4BYgi?%Zc3-o5G Date: Wed, 4 Mar 2026 16:13:11 +1000 Subject: [PATCH 065/136] Migrate Clip tests --- .../Drawing/ClipTests.cs | 70 ------------------- .../ProcessWithDrawingCanvasTests.Clip.cs | 69 ++++++++++++++++++ .../ClipTests/Clip_offset_x-20_y-100.png | 3 - .../ClipTests/Clip_offset_x-20_y-20.png | 3 - .../Drawing/ClipTests/Clip_offset_x0_y0.png | 3 - .../Drawing/ClipTests/Clip_offset_x20_y20.png | 3 - .../Drawing/ClipTests/Clip_offset_x40_y60.png | 3 - .../ClipConstrainsOperationToClipBounds.png} | 0 .../ClipOffset_offset_x-20_y-100.png | 3 + .../ClipOffset_offset_x-20_y-20.png | 3 + .../ClipOffset_offset_x0_y0.png | 3 + .../ClipOffset_offset_x20_y20.png | 3 + .../ClipOffset_offset_x40_y60.png | 3 + 13 files changed, 84 insertions(+), 85 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png rename tests/Images/ReferenceOutput/Drawing/{ClipTests/Clip_ConstrainsOperationToClipBounds.png => ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png} (100%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs deleted file mode 100644 index c8892b878..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class ClipTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)] - public void Clip(TestImageProvider provider, float dx, float dy, float sizeMult) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"offset_x{dx}_y{dy}"; - provider.RunValidatingProcessorTest( - x => - { - Size size = x.GetCurrentSize(); - int outerRadii = (int)(Math.Min(size.Width, size.Height) * sizeMult); - Star star = new(new PointF(size.Width / 2, size.Height / 2), 5, outerRadii / 2, outerRadii); - - Matrix3x2 builder = Matrix3x2.CreateTranslation(new Vector2(dx, dy)); - x.Clip(star.Transform(builder), x => x.DetectEdges()); - }, - testOutputDetails: testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)] - public void Clip_ConstrainsOperationToClipBounds(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.RunValidatingProcessorTest( - x => - { - Size size = x.GetCurrentSize(); - RectangleF rect = new(0, 0, size.Width / 2, size.Height / 2); - RectangularPolygon clipRect = new(rect); - x.Clip(clipRect, ctx => ctx.Flip(FlipMode.Vertical)); - }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - [Fact] - public void Issue250_Vertical_Horizontal_Count_Should_Match() - { - PathCollection clip = new(new RectangularPolygon(new PointF(24, 16), new PointF(777, 385))); - - Path vert = new(new LinearLineSegment(new PointF(26, 384), new PointF(26, 163))); - Path horiz = new(new LinearLineSegment(new PointF(26, 163), new PointF(176, 163))); - - IPath reverse = vert.Clip(clip); - IEnumerable> result1 = vert.Clip(reverse).Flatten().Select(x => x.Points); - - reverse = horiz.Clip(clip); - IEnumerable> result2 = horiz.Clip(reverse).Flatten().Select(x => x.Points); - - bool same = result1.Count() == result2.Count(); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs new file mode 100644 index 000000000..ba812bb5e --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Linq; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)] + public void ClipOffset(TestImageProvider provider, float dx, float dy, float sizeMult) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"offset_x{dx}_y{dy}"; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => + { + Rectangle bounds = canvas.Bounds; + int outerRadii = (int)(Math.Min(bounds.Width, bounds.Height) * sizeMult); + Star star = new(new PointF(bounds.Width / 2F, bounds.Height / 2F), 5, outerRadii / 2F, outerRadii); + Matrix3x2 builder = Matrix3x2.CreateTranslation(new Vector2(dx, dy)); + canvas.Process(star.Transform(builder), ctx => ctx.DetectEdges()); + }), + testOutputDetails: testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)] + public void ClipConstrainsOperationToClipBounds(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => + { + Rectangle bounds = canvas.Bounds; + RectangleF rect = new(0, 0, bounds.Width / 2F, bounds.Height / 2F); + RectangularPolygon clipRect = new(rect); + canvas.Process(clipRect, ctx => ctx.Flip(FlipMode.Vertical)); + }), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + [Fact] + public void ClipIssue250VerticalHorizontalCountShouldMatch() + { + PathCollection clip = new(new RectangularPolygon(new PointF(24, 16), new PointF(777, 385))); + + Path vertical = new(new LinearLineSegment(new PointF(26, 384), new PointF(26, 163))); + Path horizontal = new(new LinearLineSegment(new PointF(26, 163), new PointF(176, 163))); + + IPath reverse = vertical.Clip(clip); + int verticalCount = vertical.Clip(reverse).Flatten().Select(x => x.Points).Count(); + + reverse = horizontal.Clip(clip); + int horizontalCount = horizontal.Clip(reverse).Flatten().Select(x => x.Points).Count(); + + Assert.Equal(verticalCount, horizontalCount); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png deleted file mode 100644 index 9a791fde1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e44f9598e2f6c9a5f3aac6dcd73edb1a818d1e864fd154371b0d54ca075aa05e -size 3694 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png deleted file mode 100644 index d60adda71..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ecf41b05a42a6f275524131bcaf89298a059e2a0aabbaf2348ce2ad036197ede -size 5013 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png deleted file mode 100644 index ee0f3d4fd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:15622bb81ee71518a2fa56e758f2df5fddee69e0a01f2617e0f67201e930553f -size 5356 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png deleted file mode 100644 index 55715e2aa..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3739ab0effb4caf5e84add7c0c1d1cc3bbec0c1fb7e7d7826a818bf0976fbe4f -size 5446 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png deleted file mode 100644 index 3d61682c4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd217c38b95baedd42064b696d975805120d91561c8d77248b749d35c1fbcf75 -size 2315 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png new file mode 100644 index 000000000..be3036ed9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79010cd787cfa5828251f46ef014d8e536387d13a99f52ae93c27536546b2b26 +size 5338 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png new file mode 100644 index 000000000..f3165177f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ec0bb5cb536ac9384d5dc00485557c4fb7e485ab2eabaebf8f3ed290ebbfc8b +size 6657 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png new file mode 100644 index 000000000..d330434d9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7f4e06b5e41aefcaed4266c0a978beb2b508da15a1607d9fa0fbd08dd69a4ea +size 7002 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png new file mode 100644 index 000000000..c555afc97 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75c788a96bdba11d1e957ae1267e4bb5a1c3bd84c4eeba0cab9ec5b98066a87f +size 7032 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png new file mode 100644 index 000000000..3c11beb38 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8739bdb6ac8a50dcdafd1c8ce4e506659345c377e69af5aae57ba8007b91b837 +size 4591 From 7f5f3d687fb67ed6f855ac35d9dac2e8a79f9a77 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 16:18:57 +1000 Subject: [PATCH 066/136] Migrate robustness tests --- ...ocessWithDrawingCanvasTests.Robustness.cs} | 95 +++++++++++-------- ...Json_Mississippi_LinesScaled_Scale(10).png | 3 - ...oJson_Mississippi_LinesScaled_Scale(3).png | 3 - ...oJson_Mississippi_LinesScaled_Scale(5).png | 3 - ...oJson_Mississippi_Lines_PixelOffset(0).png | 3 - ...on_Mississippi_Lines_PixelOffset(5500).png | 3 - .../LargeGeoJson_States_Fill.png | 3 - ...Json_Mississippi_LinesScaled_Scale(10).png | 3 + ...oJson_Mississippi_LinesScaled_Scale(3).png | 3 + ...oJson_Mississippi_LinesScaled_Scale(5).png | 3 + ...oJson_Mississippi_Lines_PixelOffset(0).png | 3 + ...on_Mississippi_Lines_PixelOffset(5500).png | 3 + .../LargeGeoJson_States_Fill.png | 3 + 13 files changed, 74 insertions(+), 57 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Drawing/DrawingRobustnessTests.cs => Processing/ProcessWithDrawingCanvasTests.Robustness.cs} (84%) delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs similarity index 84% rename from tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs index 6d230ee2c..9a8b45be2 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs @@ -4,6 +4,7 @@ #pragma warning disable xUnit1004 // Test methods should not be skipped using System.Numerics; using System.Runtime.InteropServices; +using System.Linq; using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; @@ -13,10 +14,9 @@ using SixLabors.ImageSharp.Processing; using SkiaSharp; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -[GroupOutput("Drawing")] -public class DrawingRobustnessTests +public partial class ProcessWithDrawingCanvasTests { [Theory(Skip = "For local testing")] [WithSolidFilledImages(32, 32, "Black", PixelTypes.Rgba32)] @@ -41,7 +41,7 @@ public void CompareToSkiaResults_StarCircle(TestImageProvider provider) private static void CompareToSkiaResultsImpl(TestImageProvider provider, IPath shape) { using Image image = provider.GetImage(); - image.Mutate(c => c.Fill(Color.White, shape)); + image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(shape, Brushes.Solid(Color.White)))); image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); using SKBitmap bitmap = new(new SKImageInfo(image.Width, image.Height)); @@ -88,12 +88,17 @@ public void LargeGeoJson_Lines(TestImageProvider provider, string geoJso using Image image = provider.GetImage(); DrawingOptions options = new() { - GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0 }, + GraphicsOptions = new GraphicsOptions { Antialias = aa > 0 }, }; - foreach (PointF[] loop in points) + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => { - image.Mutate(c => c.DrawLine(options, Color.White, 1.0f, loop)); - } + Pen pen = Pens.Solid(Color.White, 1.0F); + foreach (PointF[] loop in points) + { + canvas.DrawLine(pen, loop); + } + })); string details = $"_{System.IO.Path.GetFileName(geoJsonFile)}_{sx}x{sy}_aa{aa}"; @@ -124,17 +129,21 @@ private static Image FillGeoJsonPolygons(TestImageProvider provi Image image = provider.GetImage(); DrawingOptions options = new() { - GraphicsOptions = new GraphicsOptions() { Antialias = aa }, + GraphicsOptions = new GraphicsOptions { Antialias = aa }, }; Random rnd = new(42); byte[] rgb = new byte[3]; - foreach (PointF[] loop in points) + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => { - rnd.NextBytes(rgb); + foreach (PointF[] loop in points) + { + rnd.NextBytes(rgb); - Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); - image.Mutate(c => c.FillPolygon(options, color, loop)); - } + Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); + canvas.Fill(new Polygon(new LinearLineSegment(loop)), Brushes.Solid(color)); + } + })); return image; } @@ -156,10 +165,14 @@ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, i IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); - foreach (PointF[] loop in points) + image.Mutate(c => c.ProcessWithCanvas(canvas => { - image.Mutate(c => c.DrawLine(Color.White, 1.0f, loop)); - } + Pen pen = Pens.Solid(Color.White, 1.0F); + foreach (PointF[] loop in points) + { + canvas.DrawLine(pen, loop); + } + })); // Strict comparer, because the image is sparse: ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); @@ -186,13 +199,17 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); - var pen = new SolidPen(new SolidBrush(Color.White), 1.0f); - foreach (PointF[] loop in points) + SolidPen pen = new(new SolidBrush(Color.White), 1.0f); + + image.Mutate(c => c.ProcessWithCanvas(canvas => { - IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); - outline = outline.Transform(Matrix3x2.CreateScale(scale, scale)); - image.Mutate(c => c.Fill(pen.StrokeFill, outline)); - } + foreach (PointF[] loop in points) + { + IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); + outline = outline.Transform(Matrix3x2.CreateScale(scale, scale)); + canvas.Fill(outline, pen.StrokeFill); + } + })); // Strict comparer, because the image is sparse: ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); @@ -273,14 +290,14 @@ public void LargeGeoJson_States_Separate_Benchmark(TestImageProvider pro using Image image = provider.GetImage(); - image.Mutate( - c => + image.Mutate(c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.White, thickness); + foreach (PointF[] loop in points) { - foreach (PointF[] loop in points) - { - c.DrawPolygon(Color.White, thickness, loop); - } - }); + canvas.Draw(pen, new Polygon(new LinearLineSegment(loop))); + } + })); image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } @@ -313,7 +330,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider image.Mutate(c => { c.SetRasterizer(DefaultRasterizer.Instance); - c.Draw(Color.White, thickness, path); + c.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(Color.White, thickness), path)); }); image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -325,18 +342,18 @@ public void LargeStar_Benchmark(TestImageProvider provider, int thicknes { List points = CreateStarPolygon(1001, 100F); Matrix3x2 transform = Matrix3x2.CreateTranslation(250, 250); + DrawingOptions options = new() { Transform = transform }; using Image image = provider.GetImage(); - image.Mutate( - c => + image.Mutate(c => c.ProcessWithCanvas(options, canvas => + { + Pen pen = Pens.Solid(Color.White, thickness); + foreach (PointF[] loop in points) { - foreach (PointF[] loop in points) - { - c.SetDrawingTransform(transform); - c.DrawPolygon(Color.White, thickness, loop); - } - }); + canvas.Draw(pen, new Polygon(new LinearLineSegment(loop))); + } + })); image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png deleted file mode 100644 index 652850f5d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a837b1b94ddc2813b0feaeffabc22c90df4bd4fdaf282c229241b0316e5621b7 -size 77807 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png deleted file mode 100644 index c622d0bfd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a282acfa163f23bd7e1a6d97c174ff290afb3edbf6b8a6f65dbcca2b7e0fa8c -size 16748 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png deleted file mode 100644 index 646002b00..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68cfa2c39e498a8c147a9fe5ca4dff10d3b53a5a5ce23bfdd3e7b7915fcff8cf -size 32709 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png deleted file mode 100644 index a3d1fd999..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cab703fe17ffd19264e0ca155945aa7c1d0bc4c6317bc87e6d64e513368e0f85 -size 4429 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png deleted file mode 100644 index 4431a489a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7eaef6cc66cd48c391fda1775da6594728de9f16cf0b9a4718ce312841624f73 -size 40967 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png deleted file mode 100644 index 6ea570a90..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:85f9dc073233b4703db8ab4df049de3d551912104863bf89756141c61667083a -size 386553 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png new file mode 100644 index 000000000..c4e08730e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:677bd21bf95fba0e6258d397e7d9989ad7457c013b04f79bfd4bfb3b93fb5556 +size 235022 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png new file mode 100644 index 000000000..c4846cc2a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cccaa9676afb69e361559cd23029c706d69bc78ad719e83b7ad1f99fbfd50110 +size 51543 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png new file mode 100644 index 000000000..da2cce583 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8308b029f57a6afad50e2c5cc77d8ff725f951e4a5626d8950f9364d04dec868 +size 97573 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png new file mode 100644 index 000000000..79e70fc51 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b51ce4dea1dc1a906d8b7b668370c296a47bb60241d8145847b622bf24c92363 +size 12064 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png new file mode 100644 index 000000000..69f16cb59 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97dd990daefdfcc8b0bab788b4e43c15c3d99f38972787946df28668a59a6d79 +size 162968 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png new file mode 100644 index 000000000..87b5da525 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d63caefa6f9131f14084a3df4a3885b73e8af0b4b3a7aca24e322732f3bb878c +size 456461 From 1dd49afc313e94517dd45172076d10ab0caf7622 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 16:34:49 +1000 Subject: [PATCH 067/136] Migrate Issues tests --- .../Issues/Issue_241.cs | 2 +- .../Issues/Issue_270.cs | 2 +- .../Issues/Issue_28_108.cs | 67 +++++++++------- .../Issues/Issue_323.cs | 33 ++++---- .../Issues/Issue_330.cs | 78 +++++++++---------- .../Issues/Issue_37.cs | 26 +++---- .../Issues/Issue_46.cs | 3 +- .../Issues/Issue_462.cs | 4 +- .../Issues/Issue_54.cs | 2 +- .../Issues/Issues_55_59.cs | 6 +- ...Rgba32_Solid2084x2084_(138,43,226,255).png | 4 +- ...d492x360_(255,255,255,255)_ColrV1-draw.png | 4 +- ...d492x360_(255,255,255,255)_ColrV1-fill.png | 4 +- ...olid492x360_(255,255,255,255)_Svg-draw.png | 4 +- ...olid492x360_(255,255,255,255)_Svg-fill.png | 4 +- 15 files changed, 127 insertions(+), 116 deletions(-) diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs index 1d208a0da..a29b32676 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs @@ -27,6 +27,6 @@ public void DoesNotThrowArgumentOutOfRangeException() const string content = "TEST"; using Image image = new Image(512, 256, Color.Black.ToPixel()); - image.Mutate(x => x.DrawText(opt, content, Brushes.Horizontal(Color.Orange))); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(opt, content, Brushes.Horizontal(Color.Orange), pen: null))); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs index 43ad525be..67f3c52f3 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs @@ -33,7 +33,7 @@ public void DoesNotThrowArgumentOutOfRangeException() using Image imageBrushImage = new(sourceImageWidth, sourceImageHeight, Color.Black.ToPixel()); ImageBrush imageBrush = new(imageBrushImage); - targetImage.Mutate(x => x.DrawText(CreateTextOptions(font, targetImageWidth), text, imageBrush, pen)); + targetImage.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(CreateTextOptions(font, targetImageWidth), text, imageBrush, pen))); } private static RichTextOptions CreateTextOptions(Font font, int wrappingLength) diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs index d18810746..f05526c45 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs @@ -9,8 +9,6 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_28_108 { - private Rgba32 red = Color.Red.ToPixel(); - [Theory] [InlineData(1F)] [InlineData(1.5F)] @@ -19,13 +17,13 @@ public class Issue_28_108 public void DrawingLineAtTopShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 0), - new PointF(100, 0))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 0), + new PointF(100, 0)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -39,13 +37,13 @@ public void DrawingLineAtTopShouldDisplay(float stroke) public void DrawingLineAtBottomShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 99), - new PointF(100, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 99), + new PointF(100, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 99)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -59,13 +57,13 @@ public void DrawingLineAtBottomShouldDisplay(float stroke) public void DrawingLineAtLeftShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 0), - new PointF(0, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 0), + new PointF(0, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 0, y: i)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -79,15 +77,24 @@ public void DrawingLineAtLeftShouldDisplay(float stroke) public void DrawingLineAtRightShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(99, 0), - new PointF(99, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(99, 0), + new PointF(99, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 99, y: i)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } + + private static DrawingOptions CreateAliasedDrawingOptions() => + new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false + } + }; } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs index c77648fe9..e605ca4f2 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs @@ -18,15 +18,15 @@ public void DrawPolygonMustDrawoutlineOnly(TestImageProvider pro where TPixel : unmanaged, IPixel { Color color = Color.RebeccaPurple; + PointF[] points = + [ + new(5, 5), + new(5, 150), + new(190, 150), + ]; + provider.RunValidatingProcessorTest( - x => x.DrawPolygon( - color, - scale, - [ - new(5, 5), - new(5, 150), - new(190, 150), - ]), + x => x.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(color, scale), new Polygon(points))), new { scale }); } @@ -40,15 +40,16 @@ public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider { Color color = Color.RebeccaPurple; + PointF[] points = + [ + new(5, 5), + new(5, 150), + new(190, 150), + ]; + PatternPen pen = Pens.DashDot(color, scale); provider.RunValidatingProcessorTest( - x => x.DrawPolygon( - pen, - [ - new(5, 5), - new(5, 150), - new(190, 150), - ]), - new { scale }); + x => x.ProcessWithCanvas(canvas => canvas.Draw(pen, new Polygon(points))), + new { scale }); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs index 26e151ddd..3fb1e24c5 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs @@ -19,46 +19,46 @@ public void OffsetTextOutlines(TestImageProvider provider) Font bibfont = fontFamily.CreateFont(600, FontStyle.Bold); Font namefont = fontFamily.CreateFont(140, FontStyle.Bold); - provider.RunValidatingProcessorTest(p => - { - p.DrawText( - new RichTextOptions(bibfont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 1024), - }, - "9999", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 20)); + provider.RunValidatingProcessorTest(p => p.ProcessWithCanvas(canvas => + { + canvas.DrawText( + new RichTextOptions(bibfont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 1024), + }, + "9999", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 20)); - p.DrawText( - new RichTextOptions(namefont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 713), - }, - "JOHAN", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 5)); + canvas.DrawText( + new RichTextOptions(namefont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 713), + }, + "JOHAN", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 5)); - p.DrawText( - new RichTextOptions(namefont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 1381), - }, - "TIGERTECH", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 5)); - }); + canvas.DrawText( + new RichTextOptions(namefont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 1381), + }, + "TIGERTECH", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 5)); + })); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs index 31e156033..0748409c8 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs @@ -23,20 +23,20 @@ public void CanRenderLargeFont() Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", 40, Fonts.FontStyle.Regular); GraphicsOptions graphicsOptions = new() { Antialias = false }; + DrawingOptions drawingOptions = new() { GraphicsOptions = graphicsOptions }; + RichTextOptions textOptions = new(font) { Origin = new PointF(50, 50) }; image.Mutate( - x => x.BackgroundColor(Color.White) - .DrawLine( - new DrawingOptions { GraphicsOptions = graphicsOptions }, - Color.Black, - 1, - new PointF(0, 50), - new PointF(150, 50)) - .DrawText( - new DrawingOptions { GraphicsOptions = graphicsOptions }, - text, - font, - Color.Black, - new PointF(50, 50))); + x => x.ProcessWithCanvas( + drawingOptions, + canvas => + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawLine( + Pens.Solid(Color.Black, 1), + new PointF(0, 50), + new PointF(150, 50)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + })); } } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs index 41e6d410c..fe2f43322 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs @@ -32,7 +32,8 @@ public void CanRenderCustomFont() float textX = ((imageSize - rect.Width) * 0.5F) + rect.Left; float textY = ((imageSize - rect.Height) * 0.5F) + (rect.Top * 0.25F); - image.Mutate(x => x.DrawText(iconText, font, Color.Black, new PointF(textX, textY))); + RichTextOptions textOptions = new(font) { Origin = new PointF(textX, textY) }; + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, iconText, Brushes.Solid(Color.Black), pen: null))); image.Save(TestFontUtilities.GetPath("e96.png")); } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs index 378b0f53a..25087a427 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs @@ -42,7 +42,7 @@ public void CanDrawEmojiFont(TestImageProvider provider, ColorFo }; provider.RunValidatingProcessorTest( - c => c.DrawText(options, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => canvas.DrawText(options, text, Brushes.Solid(Color.Black), pen: null)), testOutputDetails: $"{support}-draw", comparer: ImageComparer.TolerantPercentage(0.002f)); @@ -50,7 +50,7 @@ public void CanDrawEmojiFont(TestImageProvider provider, ColorFo c => { Pen pen = Pens.Solid(Color.Black, 2); - c.Fill(pen.StrokeFill, pen, TextBuilder.GenerateGlyphs(text, options)); + c.ProcessWithCanvas(canvas => canvas.DrawGlyphs(pen.StrokeFill, pen, TextBuilder.GenerateGlyphs(text, options))); }, testOutputDetails: $"{support}-fill", comparer: ImageComparer.TolerantPercentage(0.002f)); diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs index 32e594f8b..7393b8616 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs @@ -37,7 +37,7 @@ public void CanDrawWithoutMemoryException() string text = "sample text"; // Draw the text - image.Mutate(x => x.DrawText(textOptions, text, brush, pen)); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, brush, pen))); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs index 1e25f7617..7aabda00c 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs @@ -24,7 +24,8 @@ public void SimplifyOutOfRangeExceptionDrawLines() ]; using Image image = new(100, 100); - image.Mutate(imageContext => imageContext.DrawLine(Color.FromPixel(new Rgba32(255, 0, 0)), 1, line)); + image.Mutate(imageContext => imageContext.ProcessWithCanvas( + canvas => canvas.DrawLine(Pens.Solid(Color.FromPixel(new Rgba32(255, 0, 0)), 1), line))); } [Fact] @@ -37,6 +38,7 @@ public void SimplifyOutOfRangeExceptionDraw() new LinearLineSegment(new PointF(592.916f, 1155.754f), new PointF(592.0153f, 1156.238f))); using Image image = new(2000, 2000); - image.Mutate(imageContext => imageContext.Draw(Color.FromPixel(new Rgba32(255, 0, 0)), 1, path)); + image.Mutate(imageContext => imageContext.ProcessWithCanvas( + canvas => canvas.Draw(Pens.Solid(Color.FromPixel(new Rgba32(255, 0, 0)), 1), path))); } } diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 473293933..66cb782f5 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69c50b96bfc9c30b3d53ca17503ed5072f0e83a0541cfe0ef5570f3549d5b1e4 -size 116690 +oid sha256:4ca5183dc6ba28a4455e4b8de50ce9a1a48acbdc964cb06d8b76da8b12c2ed9c +size 140372 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png index 6dd59fe24..a17cb3530 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:742e4bd37428a4402b097eb2e33c0cc2611cb17040a34ee1457508b630705f62 -size 31937 +oid sha256:dca1adedef43e57412f765d19b4521e064e129a65023bd2bff68963948499e8d +size 37235 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index 462ffcfc5..91a2a83c7 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:919a6c8b5be40aa3894050f033d487f90d6bd2621cfb2f337874bd20904d9603 -size 10646 +oid sha256:49a745434f58765a4f0ff0bf8b85abebe065b66e6e3f2e5f476efdc796270054 +size 22596 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png index 8cc405e45..fd6287171 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48b6a904ad0557908dd053ff357b8c10d4e279eeaf6dd9d0df40aee653ecca72 -size 31954 +oid sha256:397458a75a31312e5c6af70e8d5e006cb4a139a78218d411ed4bc5a8792f35b2 +size 37267 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index f3deebc62..e050f7ff1 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a0df948f516294d3499aaab857729635b160f6ad15adc93c81fbade0fecfce7 -size 10640 +oid sha256:f7e91666dc1855f8753998c29c97073542737e75f7af0b08aaf1a41060ff4b60 +size 22599 From a0d0bb8cf05fcfe01c25343d1190b0b213e781ce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 16:38:27 +1000 Subject: [PATCH 068/136] Migrate SVGPath tests --- .../ProcessWithDrawingCanvasTests.SvgPath.cs} | 12 ++++++++---- ...RenderSvgPath_Rgba32_Blank100x100_type-arrows.png | 3 +++ ...athRenderSvgPath_Rgba32_Blank110x50_type-wave.png | 3 +++ ...PathRenderSvgPath_Rgba32_Blank110x70_type-zag.png | 3 +++ ...hRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png | 3 +++ ...SvgPath_Rgba32_Blank500x400_type-chopped_oval.png | 3 +++ ...enderSvgPath_Rgba32_Blank500x400_type-pie_big.png | 3 +++ ...derSvgPath_Rgba32_Blank500x400_type-pie_small.png | 3 +++ ...RenderSvgPath_Rgba32_Blank100x100_type-arrows.png | 3 --- .../RenderSvgPath_Rgba32_Blank110x50_type-wave.png | 3 --- .../RenderSvgPath_Rgba32_Blank110x70_type-zag.png | 3 --- .../RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png | 3 --- ...SvgPath_Rgba32_Blank500x400_type-chopped_oval.png | 3 --- ...enderSvgPath_Rgba32_Blank500x400_type-pie_big.png | 3 --- ...derSvgPath_Rgba32_Blank500x400_type-pie_small.png | 3 --- 15 files changed, 29 insertions(+), 25 deletions(-) rename tests/ImageSharp.Drawing.Tests/{Shapes/SvgPath.cs => Processing/ProcessWithDrawingCanvasTests.SvgPath.cs} (77%) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png delete mode 100644 tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs similarity index 77% rename from tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs rename to tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs index 3b8ed47a5..87063f20d 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs @@ -5,9 +5,9 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; -public class SvgPath +public partial class ProcessWithDrawingCanvasTests { [Theory] [WithBlankImage(110, 70, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] @@ -17,14 +17,18 @@ public class SvgPath [WithBlankImage(500, 400, PixelTypes.Rgba32, "M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")] [WithBlankImage(100, 100, PixelTypes.Rgba32, "M50,50 L50,20 L80,50 z M40,60 L40,90 L10,60 z", "arrows")] [WithBlankImage(500, 400, PixelTypes.Rgba32, "M 10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10", "chopped_oval")] - public void RenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) + public void SvgPathRenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) where TPixel : unmanaged, IPixel { bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); Assert.True(parsed); provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 5, path), + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 5), path); + }), new { type = exampleImageKey }, comparer: ImageComparer.TolerantPercentage(0.0035F)); // NET 472 x86 requires higher percentage } diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png new file mode 100644 index 000000000..b06cfb14a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9452a9f060afd92ec07865020a7bec1ec7bebbe0786fec07ee222e6b1f4da460 +size 860 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png new file mode 100644 index 000000000..20306876f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a87a78717cef0b311f0539d6c308d12cd4d3f71630d5a3ed75f445e9f9ae63d4 +size 963 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png new file mode 100644 index 000000000..93de85cd5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e578c7d8ffbbb9b55615f94c8c5a552a8ed5039dd31254db173a664b2694b33 +size 902 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png new file mode 100644 index 000000000..45adf807f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7dc775d74c97666a4acf55d2ea6e1a2e1759534a8e2a9c0b7adfad9055ef34f +size 9329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png new file mode 100644 index 000000000..e0579d33b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e035406af73431431322375302852d9c6e45d6a7d5a4eef6fd6c50cb733e158d +size 5289 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png new file mode 100644 index 000000000..990cd474a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36c189c6914e2f52dc41f2f0417ef27df5989d5ec89ffee950cfa31d2466415e +size 5193 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png new file mode 100644 index 000000000..2c0a3d926 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf38018dbb981504f346d1da7acf323de3dc19f0c21e8279e5b7be9a5024019f +size 9572 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png deleted file mode 100644 index 9993d5d5a..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f7ff95b1daf10aaa3579fdfab07fb8ec570fe1f1ce4fb5f553d04f29dfda255 -size 407 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png deleted file mode 100644 index f61f6ff2c..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b743c5edc9dc9478bdd8eeeea356b4c15c33942415eb0546cd2693453476eed1 -size 647 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png deleted file mode 100644 index c1a2333a9..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4a58002ef2a2f39aee947a2cac4096e1dbeeb597564d049d2bec9de45585835 -size 470 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png deleted file mode 100644 index 7fea71a75..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d45851a1743d5ebfda9cf3f6ba3f12627633954dea069a106dc9c01ee5458173 -size 4829 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png deleted file mode 100644 index 429f4440c..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6503d5ecc224260ce158fbb8775293183220b9be20acf47bcfec1e4f482682ad -size 2746 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png deleted file mode 100644 index 00af7f35d..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:16a17b87c0c302475c51472d93fa038dc39827317489c67f550a986450e35c98 -size 2428 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png deleted file mode 100644 index cfbbe58a6..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:576d8476345f085444183cbcb8c61fd03c082113dfb32cc5d9f4859d86fc5be2 -size 4765 From fd0babfbfb08e2dc44321d70d5123ddc90b99892 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 16:44:03 +1000 Subject: [PATCH 069/136] Cleanup references --- .../ClearAlwaysOverridesPreviousColor_Blue.png | Bin 123 -> 0 bytes .../ClearAlwaysOverridesPreviousColor_Khaki.png | Bin 123 -> 0 bytes .../DoesNotDependOnSinglePixelType_Argb32.png | Bin 123 -> 0 bytes .../DoesNotDependOnSinglePixelType_Rgba32.png | Bin 123 -> 0 bytes ...DoesNotDependOnSinglePixelType_RgbaVector.png | Bin 123 -> 0 bytes .../DoesNotDependOnSize_Blank16x7.png | Bin 119 -> 0 bytes .../DoesNotDependOnSize_Blank1x1.png | Bin 107 -> 0 bytes .../DoesNotDependOnSize_Blank33x32.png | Bin 142 -> 0 bytes .../DoesNotDependOnSize_Blank400x500.png | Bin 1527 -> 0 bytes .../DoesNotDependOnSize_Blank7x4.png | Bin 116 -> 0 bytes ...32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | Bin 134 -> 0 bytes ...32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | Bin 133 -> 0 bytes ...32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | Bin 134 -> 0 bytes ...32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | Bin 133 -> 0 bytes ...nColorIsOpaque_OverridePreviousColor_Blue.png | Bin 123 -> 0 bytes ...ColorIsOpaque_OverridePreviousColor_Khaki.png | Bin 123 -> 0 bytes ...ased_Rgba32_Solid400x75_(255,255,255,255).png | 3 --- 17 files changed, 3 deletions(-) delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank16x7.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png delete mode 100644 tests/Images/ReferenceOutput/RasterizerExtensionsTests/AntialiasingIsAntialiased_Rgba32_Solid400x75_(255,255,255,255).png diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png deleted file mode 100644 index dad8ece493e457988496bb8685370ad5b3d158cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVk diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png deleted file mode 100644 index 3fc305e9fe57b5da980d999a622c31f2e4637e3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoVmdKI;Vst0GIw8j{pDw diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png deleted file mode 100644 index 4e4ee1ee16f63bedde274c4348fa889381ce74d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blmUKs7M+SzC{oH>NS%G}c0*}aI z1_r*vAk26?e?bP0l+XkKU>q4_ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png deleted file mode 100644 index 31965cc3a09475f5ebc940c2dac1aaff370aba85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^ia@Nu!3HGf><~N!q*&4&eH|GXHuiJ>Nn{1`ISV`@ ziy0XB4ude`@%$AjKtTgf7srr_TW`-9G6Hol7_@Kt_0pZEPxoNaD)qip8wEuaP?*a2 XO@Mh$(Dd%bK)no}u6{1-oD!M<@uVb5 diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png deleted file mode 100644 index f3c6b080b9e4d34794fbb71a2e5493e8ee53dc8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1527 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKNUpUx+BEq+-8-Nr`x}&cn1H;CC?mvmFKt5-I zM`SSr1K(i~W;~w1B87p0b*86_V@SoVw^tPz85lSY7_g+ibhkgza41JGZ(gu1BZIpp zBLf>#LnA|i0)qqx2+@)Uu>@uVjd^fSf$gI@4>_RNm}t!#jhIFTqNC6QS?Lgu8PrTU mw6hE_0+HfyL`Teju`ds~-iR<=cnU1J89ZJ6T-G@yGywq1xbjs1 diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png deleted file mode 100644 index 8914b9c49c58f2b0ab4f1a3ab86ee7eb383d4a82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YpyIO&eQjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pqQtNV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!poXW5V@SoVgTe~DWM4fDn%po diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f3429249693995088f7f85674b3f6068ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pt`4vV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!poXW5V@SoVgTe~DWM4fDn%po diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f3429249693995088f7f85674b3f6068ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!pt`4vV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV!lvI6;>1s;*b z3=DjSL74G){)!Z!psc5hV@SoV Date: Wed, 4 Mar 2026 16:49:22 +1000 Subject: [PATCH 070/136] Optimize refs --- .../Backends/WebGPUDrawingBackendTests.cs | 16 ++++++++++++++++ ...r_WithClipPath_MatchesReference_Rgba32.png | 4 ++-- ...calCoordinates_MatchesReference_Rgba32.png | 4 ++-- ...StateIsolation_MatchesReference_Rgba32.png | 4 ++-- ...hesReference_Rgba32_ColrV1-draw-glyphs.png | 4 ++-- ...atchesReference_Rgba32_Svg-draw-glyphs.png | 4 ++-- ...thAndTransform_MatchesReference_Rgba32.png | 4 ++-- ...pingAndScaling_MatchesReference_Rgba32.png | 4 ++-- ...imitiveHelpers_MatchesReference_Rgba32.png | 4 ++-- ...PathWithOrigin_MatchesReference_Rgba32.png | 4 ++-- ..._FillAndStroke_MatchesReference_Rgba32.png | 4 ++-- ...eMetricsGuides_MatchesReference_Rgba32.png | 4 ++-- ...awText_PenOnly_MatchesReference_Rgba32.png | 4 ++-- ...AndLineSpacing_MatchesReference_Rgba32.png | 4 ++-- ...izeOutputFalse_MatchesReference_Rgba32.png | 4 ++-- ...aw_PathBuilder_MatchesReference_Rgba32.png | 4 ++-- ...ndGradientPens_MatchesReference_Rgba32.png | 4 ++-- ...ll_PathBuilder_MatchesReference_Rgba32.png | 4 ++-- ...enOddVsNonZero_MatchesReference_Rgba32.png | 4 ++-- ...PatternBrushes_MatchesReference_Rgba32.png | 4 ++-- ...backCapability_MatchesReference_Rgba32.png | 4 ++-- ...backCapability_MatchesReference_Rgba32.png | 4 ++-- .../Process_Path_MatchesReference_Rgba32.png | 4 ++-- ...MultipleStates_MatchesReference_Rgba32.png | 4 ++-- ...store_ClipPath_MatchesReference_Rgba32.png | 4 ++-- ...Ellipse_composition-Clear_blending-Add.png | 4 ++-- ...ipse_composition-Clear_blending-Darken.png | 4 ++-- ...e_composition-Clear_blending-HardLight.png | 4 ++-- ...pse_composition-Clear_blending-Lighten.png | 4 ++-- ...se_composition-Clear_blending-Multiply.png | 4 ++-- ...ipse_composition-Clear_blending-Normal.png | 4 ++-- ...pse_composition-Clear_blending-Overlay.png | 4 ++-- ...ipse_composition-Clear_blending-Screen.png | 4 ++-- ...se_composition-Clear_blending-Subtract.png | 4 ++-- ...ipse_composition-DestAtop_blending-Add.png | 4 ++-- ...e_composition-DestAtop_blending-Darken.png | 4 ++-- ...omposition-DestAtop_blending-HardLight.png | 4 ++-- ..._composition-DestAtop_blending-Lighten.png | 4 ++-- ...composition-DestAtop_blending-Multiply.png | 4 ++-- ...e_composition-DestAtop_blending-Normal.png | 4 ++-- ..._composition-DestAtop_blending-Overlay.png | 4 ++-- ...e_composition-DestAtop_blending-Screen.png | 4 ++-- ...composition-DestAtop_blending-Subtract.png | 4 ++-- ...ipse_composition-DestOver_blending-Add.png | 4 ++-- ...e_composition-DestOver_blending-Darken.png | 4 ++-- ...omposition-DestOver_blending-HardLight.png | 4 ++-- ..._composition-DestOver_blending-Lighten.png | 4 ++-- ...composition-DestOver_blending-Multiply.png | 4 ++-- ...e_composition-DestOver_blending-Normal.png | 4 ++-- ..._composition-DestOver_blending-Overlay.png | 4 ++-- ...e_composition-DestOver_blending-Screen.png | 4 ++-- ...composition-DestOver_blending-Subtract.png | 4 ++-- ...kEllipse_composition-Dest_blending-Add.png | 4 ++-- ...lipse_composition-Dest_blending-Darken.png | 4 ++-- ...se_composition-Dest_blending-HardLight.png | 4 ++-- ...ipse_composition-Dest_blending-Lighten.png | 4 ++-- ...pse_composition-Dest_blending-Multiply.png | 4 ++-- ...lipse_composition-Dest_blending-Normal.png | 4 ++-- ...ipse_composition-Dest_blending-Overlay.png | 4 ++-- ...lipse_composition-Dest_blending-Screen.png | 4 ++-- ...pse_composition-Dest_blending-Subtract.png | 4 ++-- ...lipse_composition-SrcAtop_blending-Add.png | 4 ++-- ...e_composition-SrcAtop_blending-Lighten.png | 4 ++-- ...se_composition-SrcAtop_blending-Screen.png | 4 ++-- ..._composition-SrcAtop_blending-Subtract.png | 4 ++-- ...Ellipse_composition-SrcIn_blending-Add.png | 4 ++-- ...ipse_composition-SrcIn_blending-Darken.png | 4 ++-- ...e_composition-SrcIn_blending-HardLight.png | 4 ++-- ...pse_composition-SrcIn_blending-Lighten.png | 4 ++-- ...se_composition-SrcIn_blending-Multiply.png | 4 ++-- ...ipse_composition-SrcIn_blending-Normal.png | 4 ++-- ...pse_composition-SrcIn_blending-Overlay.png | 4 ++-- ...ipse_composition-SrcIn_blending-Screen.png | 4 ++-- ...se_composition-SrcIn_blending-Subtract.png | 4 ++-- ...llipse_composition-SrcOut_blending-Add.png | 4 ++-- ...pse_composition-SrcOut_blending-Darken.png | 4 ++-- ..._composition-SrcOut_blending-HardLight.png | 4 ++-- ...se_composition-SrcOut_blending-Lighten.png | 4 ++-- ...e_composition-SrcOut_blending-Multiply.png | 4 ++-- ...pse_composition-SrcOut_blending-Normal.png | 4 ++-- ...se_composition-SrcOut_blending-Overlay.png | 4 ++-- ...pse_composition-SrcOut_blending-Screen.png | 4 ++-- ...e_composition-SrcOut_blending-Subtract.png | 4 ++-- ...lipse_composition-SrcOver_blending-Add.png | 4 ++-- ...se_composition-SrcOver_blending-Darken.png | 4 ++-- ...composition-SrcOver_blending-HardLight.png | 4 ++-- ...e_composition-SrcOver_blending-Lighten.png | 4 ++-- ..._composition-SrcOver_blending-Multiply.png | 4 ++-- ...se_composition-SrcOver_blending-Normal.png | 4 ++-- ...e_composition-SrcOver_blending-Overlay.png | 4 ++-- ...se_composition-SrcOver_blending-Screen.png | 4 ++-- ..._composition-SrcOver_blending-Subtract.png | 4 ++-- ...ckEllipse_composition-Src_blending-Add.png | 4 ++-- ...llipse_composition-Src_blending-Darken.png | 4 ++-- ...pse_composition-Src_blending-HardLight.png | 4 ++-- ...lipse_composition-Src_blending-Lighten.png | 4 ++-- ...ipse_composition-Src_blending-Multiply.png | 4 ++-- ...llipse_composition-Src_blending-Normal.png | 4 ++-- ...lipse_composition-Src_blending-Overlay.png | 4 ++-- ...llipse_composition-Src_blending-Screen.png | 4 ++-- ...ipse_composition-Src_blending-Subtract.png | 4 ++-- ...ckEllipse_composition-Xor_blending-Add.png | 4 ++-- ...llipse_composition-Xor_blending-Darken.png | 4 ++-- ...pse_composition-Xor_blending-HardLight.png | 4 ++-- ...lipse_composition-Xor_blending-Lighten.png | 4 ++-- ...ipse_composition-Xor_blending-Multiply.png | 4 ++-- ...llipse_composition-Xor_blending-Normal.png | 4 ++-- ...lipse_composition-Xor_blending-Overlay.png | 4 ++-- ...llipse_composition-Xor_blending-Screen.png | 4 ++-- ...ipse_composition-Xor_blending-Subtract.png | 4 ++-- ...Ellipse_composition-Clear_blending-Add.png | 4 ++-- ...ipse_composition-Clear_blending-Darken.png | 4 ++-- ...e_composition-Clear_blending-HardLight.png | 4 ++-- ...pse_composition-Clear_blending-Lighten.png | 4 ++-- ...se_composition-Clear_blending-Multiply.png | 4 ++-- ...ipse_composition-Clear_blending-Normal.png | 4 ++-- ...pse_composition-Clear_blending-Overlay.png | 4 ++-- ...ipse_composition-Clear_blending-Screen.png | 4 ++-- ...se_composition-Clear_blending-Subtract.png | 4 ++-- ...dEllipse_composition-Dest_blending-Add.png | 4 ++-- ...lipse_composition-Dest_blending-Darken.png | 4 ++-- ...se_composition-Dest_blending-HardLight.png | 4 ++-- ...ipse_composition-Dest_blending-Lighten.png | 4 ++-- ...pse_composition-Dest_blending-Multiply.png | 4 ++-- ...lipse_composition-Dest_blending-Normal.png | 4 ++-- ...ipse_composition-Dest_blending-Overlay.png | 4 ++-- ...lipse_composition-Dest_blending-Screen.png | 4 ++-- ...pse_composition-Dest_blending-Subtract.png | 4 ++-- ..._composition-SrcAtop_blending-Subtract.png | 4 ++-- ...Ellipse_composition-Clear_blending-Add.png | 4 ++-- ...ipse_composition-Clear_blending-Darken.png | 4 ++-- ...e_composition-Clear_blending-HardLight.png | 4 ++-- ...pse_composition-Clear_blending-Lighten.png | 4 ++-- ...se_composition-Clear_blending-Multiply.png | 4 ++-- ...ipse_composition-Clear_blending-Normal.png | 4 ++-- ...pse_composition-Clear_blending-Overlay.png | 4 ++-- ...ipse_composition-Clear_blending-Screen.png | 4 ++-- ...se_composition-Clear_blending-Subtract.png | 4 ++-- ...ipse_composition-DestAtop_blending-Add.png | 4 ++-- ...e_composition-DestAtop_blending-Darken.png | 4 ++-- ...omposition-DestAtop_blending-HardLight.png | 4 ++-- ..._composition-DestAtop_blending-Lighten.png | 4 ++-- ...composition-DestAtop_blending-Multiply.png | 4 ++-- ...e_composition-DestAtop_blending-Normal.png | 4 ++-- ..._composition-DestAtop_blending-Overlay.png | 4 ++-- ...e_composition-DestAtop_blending-Screen.png | 4 ++-- ...composition-DestAtop_blending-Subtract.png | 4 ++-- ...llipse_composition-DestIn_blending-Add.png | 4 ++-- ...pse_composition-DestIn_blending-Darken.png | 4 ++-- ..._composition-DestIn_blending-HardLight.png | 4 ++-- ...se_composition-DestIn_blending-Lighten.png | 4 ++-- ...e_composition-DestIn_blending-Multiply.png | 4 ++-- ...pse_composition-DestIn_blending-Normal.png | 4 ++-- ...se_composition-DestIn_blending-Overlay.png | 4 ++-- ...pse_composition-DestIn_blending-Screen.png | 4 ++-- ...e_composition-DestIn_blending-Subtract.png | 4 ++-- ...lipse_composition-DestOut_blending-Add.png | 4 ++-- ...se_composition-DestOut_blending-Darken.png | 4 ++-- ...composition-DestOut_blending-HardLight.png | 4 ++-- ...e_composition-DestOut_blending-Lighten.png | 4 ++-- ..._composition-DestOut_blending-Multiply.png | 4 ++-- ...se_composition-DestOut_blending-Normal.png | 4 ++-- ...e_composition-DestOut_blending-Overlay.png | 4 ++-- ...se_composition-DestOut_blending-Screen.png | 4 ++-- ..._composition-DestOut_blending-Subtract.png | 4 ++-- ...ipse_composition-DestOver_blending-Add.png | 4 ++-- ...e_composition-DestOver_blending-Darken.png | 4 ++-- ...omposition-DestOver_blending-HardLight.png | 4 ++-- ..._composition-DestOver_blending-Lighten.png | 4 ++-- ...composition-DestOver_blending-Multiply.png | 4 ++-- ...e_composition-DestOver_blending-Normal.png | 4 ++-- ..._composition-DestOver_blending-Overlay.png | 4 ++-- ...e_composition-DestOver_blending-Screen.png | 4 ++-- ...composition-DestOver_blending-Subtract.png | 4 ++-- ...tEllipse_composition-Dest_blending-Add.png | 4 ++-- ...lipse_composition-Dest_blending-Darken.png | 4 ++-- ...se_composition-Dest_blending-HardLight.png | 4 ++-- ...ipse_composition-Dest_blending-Lighten.png | 4 ++-- ...pse_composition-Dest_blending-Multiply.png | 4 ++-- ...lipse_composition-Dest_blending-Normal.png | 4 ++-- ...ipse_composition-Dest_blending-Overlay.png | 4 ++-- ...lipse_composition-Dest_blending-Screen.png | 4 ++-- ...pse_composition-Dest_blending-Subtract.png | 4 ++-- ...lipse_composition-SrcAtop_blending-Add.png | 4 ++-- ...se_composition-SrcAtop_blending-Darken.png | 4 ++-- ...composition-SrcAtop_blending-HardLight.png | 4 ++-- ...e_composition-SrcAtop_blending-Lighten.png | 4 ++-- ..._composition-SrcAtop_blending-Multiply.png | 4 ++-- ...se_composition-SrcAtop_blending-Normal.png | 4 ++-- ...e_composition-SrcAtop_blending-Overlay.png | 4 ++-- ...se_composition-SrcAtop_blending-Screen.png | 4 ++-- ..._composition-SrcAtop_blending-Subtract.png | 4 ++-- ...Ellipse_composition-SrcIn_blending-Add.png | 4 ++-- ...ipse_composition-SrcIn_blending-Darken.png | 4 ++-- ...e_composition-SrcIn_blending-HardLight.png | 4 ++-- ...pse_composition-SrcIn_blending-Lighten.png | 4 ++-- ...se_composition-SrcIn_blending-Multiply.png | 4 ++-- ...ipse_composition-SrcIn_blending-Normal.png | 4 ++-- ...pse_composition-SrcIn_blending-Overlay.png | 4 ++-- ...ipse_composition-SrcIn_blending-Screen.png | 4 ++-- ...se_composition-SrcIn_blending-Subtract.png | 4 ++-- ...llipse_composition-SrcOut_blending-Add.png | 4 ++-- ...pse_composition-SrcOut_blending-Darken.png | 4 ++-- ..._composition-SrcOut_blending-HardLight.png | 4 ++-- ...se_composition-SrcOut_blending-Lighten.png | 4 ++-- ...e_composition-SrcOut_blending-Multiply.png | 4 ++-- ...pse_composition-SrcOut_blending-Normal.png | 4 ++-- ...se_composition-SrcOut_blending-Overlay.png | 4 ++-- ...pse_composition-SrcOut_blending-Screen.png | 4 ++-- ...e_composition-SrcOut_blending-Subtract.png | 4 ++-- ...lipse_composition-SrcOver_blending-Add.png | 4 ++-- ...se_composition-SrcOver_blending-Darken.png | 4 ++-- ...composition-SrcOver_blending-HardLight.png | 4 ++-- ...e_composition-SrcOver_blending-Lighten.png | 4 ++-- ..._composition-SrcOver_blending-Multiply.png | 4 ++-- ...se_composition-SrcOver_blending-Normal.png | 4 ++-- ...e_composition-SrcOver_blending-Overlay.png | 4 ++-- ...se_composition-SrcOver_blending-Screen.png | 4 ++-- ..._composition-SrcOver_blending-Subtract.png | 4 ++-- ...ntEllipse_composition-Src_blending-Add.png | 4 ++-- ...llipse_composition-Src_blending-Darken.png | 4 ++-- ...pse_composition-Src_blending-HardLight.png | 4 ++-- ...lipse_composition-Src_blending-Lighten.png | 4 ++-- ...ipse_composition-Src_blending-Multiply.png | 4 ++-- ...llipse_composition-Src_blending-Normal.png | 4 ++-- ...lipse_composition-Src_blending-Overlay.png | 4 ++-- ...llipse_composition-Src_blending-Screen.png | 4 ++-- ...ipse_composition-Src_blending-Subtract.png | 4 ++-- ...ntEllipse_composition-Xor_blending-Add.png | 4 ++-- ...llipse_composition-Xor_blending-Darken.png | 4 ++-- ...pse_composition-Xor_blending-HardLight.png | 4 ++-- ...lipse_composition-Xor_blending-Lighten.png | 4 ++-- ...ipse_composition-Xor_blending-Multiply.png | 4 ++-- ...llipse_composition-Xor_blending-Normal.png | 4 ++-- ...lipse_composition-Xor_blending-Overlay.png | 4 ++-- ...llipse_composition-Xor_blending-Screen.png | 4 ++-- ...ipse_composition-Xor_blending-Subtract.png | 4 ++-- ...inkRect_composition-Clear_blending-Add.png | 4 ++-- ...Rect_composition-Clear_blending-Darken.png | 4 ++-- ...t_composition-Clear_blending-HardLight.png | 4 ++-- ...ect_composition-Clear_blending-Lighten.png | 4 ++-- ...ct_composition-Clear_blending-Multiply.png | 4 ++-- ...Rect_composition-Clear_blending-Normal.png | 4 ++-- ...ect_composition-Clear_blending-Overlay.png | 4 ++-- ...Rect_composition-Clear_blending-Screen.png | 4 ++-- ...ct_composition-Clear_blending-Subtract.png | 4 ++-- ...Rect_composition-DestAtop_blending-Add.png | 4 ++-- ...t_composition-DestAtop_blending-Darken.png | 4 ++-- ...omposition-DestAtop_blending-HardLight.png | 4 ++-- ..._composition-DestAtop_blending-Lighten.png | 4 ++-- ...composition-DestAtop_blending-Multiply.png | 4 ++-- ...t_composition-DestAtop_blending-Normal.png | 4 ++-- ..._composition-DestAtop_blending-Overlay.png | 4 ++-- ...t_composition-DestAtop_blending-Screen.png | 4 ++-- ...composition-DestAtop_blending-Subtract.png | 4 ++-- ...nkRect_composition-DestIn_blending-Add.png | 4 ++-- ...ect_composition-DestIn_blending-Darken.png | 4 ++-- ..._composition-DestIn_blending-HardLight.png | 4 ++-- ...ct_composition-DestIn_blending-Lighten.png | 4 ++-- ...t_composition-DestIn_blending-Multiply.png | 4 ++-- ...ect_composition-DestIn_blending-Normal.png | 4 ++-- ...ct_composition-DestIn_blending-Overlay.png | 4 ++-- ...ect_composition-DestIn_blending-Screen.png | 4 ++-- ...t_composition-DestIn_blending-Subtract.png | 4 ++-- ...kRect_composition-DestOut_blending-Add.png | 4 ++-- ...ct_composition-DestOut_blending-Darken.png | 4 ++-- ...composition-DestOut_blending-HardLight.png | 4 ++-- ...t_composition-DestOut_blending-Lighten.png | 4 ++-- ..._composition-DestOut_blending-Multiply.png | 4 ++-- ...ct_composition-DestOut_blending-Normal.png | 4 ++-- ...t_composition-DestOut_blending-Overlay.png | 4 ++-- ...ct_composition-DestOut_blending-Screen.png | 4 ++-- ..._composition-DestOut_blending-Subtract.png | 4 ++-- ...Rect_composition-DestOver_blending-Add.png | 4 ++-- ...t_composition-DestOver_blending-Darken.png | 4 ++-- ...omposition-DestOver_blending-HardLight.png | 4 ++-- ..._composition-DestOver_blending-Lighten.png | 4 ++-- ...composition-DestOver_blending-Multiply.png | 4 ++-- ...t_composition-DestOver_blending-Normal.png | 4 ++-- ..._composition-DestOver_blending-Overlay.png | 4 ++-- ...t_composition-DestOver_blending-Screen.png | 4 ++-- ...composition-DestOver_blending-Subtract.png | 4 ++-- ...PinkRect_composition-Dest_blending-Add.png | 4 ++-- ...kRect_composition-Dest_blending-Darken.png | 4 ++-- ...ct_composition-Dest_blending-HardLight.png | 4 ++-- ...Rect_composition-Dest_blending-Lighten.png | 4 ++-- ...ect_composition-Dest_blending-Multiply.png | 4 ++-- ...kRect_composition-Dest_blending-Normal.png | 4 ++-- ...Rect_composition-Dest_blending-Overlay.png | 4 ++-- ...kRect_composition-Dest_blending-Screen.png | 4 ++-- ...ect_composition-Dest_blending-Subtract.png | 4 ++-- ...kRect_composition-SrcAtop_blending-Add.png | 4 ++-- ...ct_composition-SrcAtop_blending-Darken.png | 4 ++-- ...composition-SrcAtop_blending-HardLight.png | 4 ++-- ...t_composition-SrcAtop_blending-Lighten.png | 4 ++-- ..._composition-SrcAtop_blending-Multiply.png | 4 ++-- ...ct_composition-SrcAtop_blending-Normal.png | 4 ++-- ...t_composition-SrcAtop_blending-Overlay.png | 4 ++-- ...ct_composition-SrcAtop_blending-Screen.png | 4 ++-- ..._composition-SrcAtop_blending-Subtract.png | 4 ++-- ...inkRect_composition-SrcIn_blending-Add.png | 4 ++-- ...Rect_composition-SrcIn_blending-Darken.png | 4 ++-- ...t_composition-SrcIn_blending-HardLight.png | 4 ++-- ...ect_composition-SrcIn_blending-Lighten.png | 4 ++-- ...ct_composition-SrcIn_blending-Multiply.png | 4 ++-- ...Rect_composition-SrcIn_blending-Normal.png | 4 ++-- ...ect_composition-SrcIn_blending-Overlay.png | 4 ++-- ...Rect_composition-SrcIn_blending-Screen.png | 4 ++-- ...ct_composition-SrcIn_blending-Subtract.png | 4 ++-- ...nkRect_composition-SrcOut_blending-Add.png | 4 ++-- ...ect_composition-SrcOut_blending-Darken.png | 4 ++-- ..._composition-SrcOut_blending-HardLight.png | 4 ++-- ...ct_composition-SrcOut_blending-Lighten.png | 4 ++-- ...t_composition-SrcOut_blending-Multiply.png | 4 ++-- ...ect_composition-SrcOut_blending-Normal.png | 4 ++-- ...ct_composition-SrcOut_blending-Overlay.png | 4 ++-- ...ect_composition-SrcOut_blending-Screen.png | 4 ++-- ...t_composition-SrcOut_blending-Subtract.png | 4 ++-- ...kRect_composition-SrcOver_blending-Add.png | 4 ++-- ...ct_composition-SrcOver_blending-Darken.png | 4 ++-- ...composition-SrcOver_blending-HardLight.png | 4 ++-- ...t_composition-SrcOver_blending-Lighten.png | 4 ++-- ..._composition-SrcOver_blending-Multiply.png | 4 ++-- ...ct_composition-SrcOver_blending-Normal.png | 4 ++-- ...t_composition-SrcOver_blending-Overlay.png | 4 ++-- ...ct_composition-SrcOver_blending-Screen.png | 4 ++-- ..._composition-SrcOver_blending-Subtract.png | 4 ++-- ...tPinkRect_composition-Src_blending-Add.png | 4 ++-- ...nkRect_composition-Src_blending-Darken.png | 4 ++-- ...ect_composition-Src_blending-HardLight.png | 4 ++-- ...kRect_composition-Src_blending-Lighten.png | 4 ++-- ...Rect_composition-Src_blending-Multiply.png | 4 ++-- ...nkRect_composition-Src_blending-Normal.png | 4 ++-- ...kRect_composition-Src_blending-Overlay.png | 4 ++-- ...nkRect_composition-Src_blending-Screen.png | 4 ++-- ...Rect_composition-Src_blending-Subtract.png | 4 ++-- ...tPinkRect_composition-Xor_blending-Add.png | 4 ++-- ...nkRect_composition-Xor_blending-Darken.png | 4 ++-- ...ect_composition-Xor_blending-HardLight.png | 4 ++-- ...kRect_composition-Xor_blending-Lighten.png | 4 ++-- ...Rect_composition-Xor_blending-Multiply.png | 4 ++-- ...nkRect_composition-Xor_blending-Normal.png | 4 ++-- ...kRect_composition-Xor_blending-Overlay.png | 4 ++-- ...nkRect_composition-Xor_blending-Screen.png | 4 ++-- ...Rect_composition-Xor_blending-Subtract.png | 4 ++-- ...anDrawTextVertical2_Rgba32_Blank48x935.png | 4 ++-- ...wTextVerticalMixed2_Rgba32_Blank48x839.png | 4 ++-- ...wTextVerticalMixed_Rgba32_Blank500x400.png | 4 ++-- ...anDrawTextVertical_Rgba32_Blank500x400.png | 4 ++-- ...lTextVerticalMixed_Rgba32_Blank500x400.png | 4 ++-- ...anFillTextVertical_Rgba32_Blank500x400.png | 4 ++-- ...-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 4 ++-- ...-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 4 ++-- .../ClipConstrainsOperationToClipBounds.png | 4 ++-- .../ClipOffset_offset_x-20_y-100.png | 4 ++-- .../ClipOffset_offset_x-20_y-20.png | 4 ++-- .../ClipOffset_offset_x0_y0.png | 4 ++-- .../ClipOffset_offset_x20_y20.png | 4 ++-- .../ClipOffset_offset_x40_y60.png | 4 ++-- .../DrawComplexPolygon.png | 4 ++-- .../DrawComplexPolygon__Dashed.png | 4 ++-- .../DrawComplexPolygon__Overlap.png | 4 ++-- .../DrawComplexPolygon__Transparent.png | 4 ++-- .../DrawLinesInvalidPoints_Rgba32_T(1).png | 4 ++-- ...sInvalidPoints_Rgba32_T(1)_NoAntialias.png | 4 ++-- .../DrawLinesInvalidPoints_Rgba32_T(5).png | 4 ++-- ...sInvalidPoints_Rgba32_T(5)_NoAntialias.png | 4 ++-- .../DrawPathClippedOnTop.png | 4 ++-- .../DrawPath_HotPink_A150_T5.png | 4 ++-- .../DrawPath_HotPink_A255_T5.png | 4 ++-- .../DrawPath_Red_A255_T3.png | 4 ++-- .../DrawPath_White_A255_T1.5.png | 4 ++-- .../DrawPath_White_A255_T15.png | 4 ++-- ...sformed_Rgba32_BasicTestPattern100x100.png | 4 ++-- .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | 4 ++-- ...gon_Rgba32_White_A(1)_T(5)_NoAntialias.png | 4 ++-- ...sformed_Rgba32_BasicTestPattern250x350.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_0.10.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_0.80.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_1.00.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_1.20.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_1.60.png | 4 ++-- ...arallelEllipsesWithDifferentRatio_2.00.png | 4 ++-- ...lipsesWithDifferentRatio_0.10_AT_00deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.10_AT_30deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.10_AT_45deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.10_AT_90deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.40_AT_30deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.40_AT_45deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.80_AT_00deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.80_AT_30deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.80_AT_45deg.png | 4 ++-- ...lipsesWithDifferentRatio_0.80_AT_90deg.png | 4 ++-- ...lipsesWithDifferentRatio_1.00_AT_00deg.png | 4 ++-- ...lipsesWithDifferentRatio_1.00_AT_30deg.png | 4 ++-- ...lipsesWithDifferentRatio_1.00_AT_45deg.png | 4 ++-- ...lipsesWithDifferentRatio_1.00_AT_90deg.png | 4 ++-- ...ushWithEqualColorsReturnsUnicolorImage.png | 4 ++-- ...ImageBrushCanDrawLandscapeImage_Rgba32.png | 4 ++-- ...illImageBrushCanDrawOffsetImage_Rgba32.png | 4 ++-- ...lImageBrushCanDrawPortraitImage_Rgba32.png | 4 ++-- .../FillImageBrushCanOffsetImage_Rgba32.png | 4 ++-- ...mageBrushCanOffsetViaBrushImage_Rgba32.png | 4 ++-- ...ushUseBrushOfDifferentPixelType_Bgra32.png | 4 ++-- ...ushUseBrushOfDifferentPixelType_Rgba32.png | 4 ++-- ...0080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png | 4 ++-- ..._[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png | 4 ++-- ...EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png | 4 ++-- ...EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png | 4 ++-- ...shBrushApplicatorIsThreadSafeIssue1044.png | 4 ++-- ...hDoesNotDependOnSinglePixelType_Argb32.png | 4 ++-- ...shDoesNotDependOnSinglePixelType_Rgb24.png | 4 ++-- ...hDoesNotDependOnSinglePixelType_Rgba32.png | 4 ++-- ...rushHorizontalGradientWithRepMode_None.png | 4 ++-- ...hHorizontalGradientWithRepMode_Reflect.png | 4 ++-- ...shHorizontalGradientWithRepMode_Repeat.png | 4 ++-- ...tBrushHorizontalReturnsUnicolorColumns.png | 4 ++-- ...tBrushVerticalBrushReturnsUnicolorRows.png | 4 ++-- ...StopsProduceDashedPatterns_0.1_0.3_0.6.png | 4 ++-- ...sProduceDashedPatterns_0.2_0.4_0.6_0.8.png | 4 ++-- ...hDoubledStopsProduceDashedPatterns_0.5.png | 4 ++-- ...ushWithEqualColorsReturnsUnicolorImage.png | 4 ++-- ...cleOutsideBoundsDrawingArea_(-110_-20).png | 4 ++-- ...cleOutsideBoundsDrawingArea_(-110_-49).png | 4 ++-- ...cleOutsideBoundsDrawingArea_(-110_-50).png | 4 ++-- ...cleOutsideBoundsDrawingArea_(-110_-60).png | 4 ++-- ...ircleOutsideBoundsDrawingArea_(-110_0).png | 4 ++-- ...CircleOutsideBoundsDrawingArea_(-99_0).png | 4 ++-- ...CircleOutsideBoundsDrawingArea_(0_-50).png | 4 ++-- ...CircleOutsideBoundsDrawingArea_(0_-60).png | 4 ++-- ...rcleOutsideBoundsDrawingArea_(110_-49).png | 4 ++-- ...rcleOutsideBoundsDrawingArea_(110_-50).png | 4 ++-- ...rcleOutsideBoundsDrawingArea_(110_-60).png | 4 ++-- .../FillPathGradientBrushFillComplex.png | 4 ++-- ...tBrushFillRectangleWithDifferentColors.png | 4 ++-- ...eWithDifferentColors_Rgba32_Blank10x10.png | 4 ++-- ...ntBrushFillTriangleWithDifferentColors.png | 4 ++-- ...hFillTriangleWithDifferentColorsCenter.png | 4 ++-- ...ifferentColorsCenter_Rgba32_Blank20x20.png | 4 ++-- ...eWithDifferentColors_Rgba32_Blank20x20.png | 4 ++-- ...GradientBrushFillTriangleWithGreyscale.png | 4 ++-- ...gleWithGreyscale_HalfSingle_Blank20x20.png | 4 ++-- ...GradientBrushFillWithCustomCenterColor.png | 4 ++-- ...ithCustomCenterColor_Rgba32_Blank10x10.png | 4 ++-- ...dRotateTheColorsWhenThereAreMorePoints.png | 4 ++-- ...enThereAreMorePoints_Rgba32_Blank10x10.png | 4 ++-- ...houldBeFloodFilledWithBackwardDiagonal.png | 4 ++-- ...dFilledWithBackwardDiagonalTransparent.png | 4 ++-- ...ShouldBeFloodFilledWithForwardDiagonal.png | 4 ++-- ...odFilledWithForwardDiagonalTransparent.png | 4 ++-- ...ImageShouldBeFloodFilledWithHorizontal.png | 4 ++-- ...BeFloodFilledWithHorizontalTransparent.png | 4 ++-- ...rnBrushImageShouldBeFloodFilledWithMin.png | 4 ++-- ...eShouldBeFloodFilledWithMinTransparent.png | 4 ++-- ...hImageShouldBeFloodFilledWithPercent10.png | 4 ++-- ...dBeFloodFilledWithPercent10Transparent.png | 4 ++-- ...hImageShouldBeFloodFilledWithPercent20.png | 4 ++-- ...dBeFloodFilledWithPercent20Transparent.png | 4 ++-- ...shImageShouldBeFloodFilledWithVertical.png | 4 ++-- ...ldBeFloodFilledWithVerticalTransparent.png | 4 ++-- ...verse(False)_IntersectionRule(EvenOdd).png | 4 ++-- ...verse(False)_IntersectionRule(NonZero).png | 4 ++-- ...everse(True)_IntersectionRule(EvenOdd).png | 4 ++-- ...everse(True)_IntersectionRule(NonZero).png | 4 ++-- .../FillPolygon_Concave_Reverse(False).png | 4 ++-- .../FillPolygon_Concave_Reverse(True).png | 4 ++-- ...verse(False)_IntersectionRule(EvenOdd).png | 4 ++-- ...verse(False)_IntersectionRule(NonZero).png | 4 ++-- ...everse(True)_IntersectionRule(EvenOdd).png | 4 ++-- ...everse(True)_IntersectionRule(NonZero).png | 4 ++-- ...olygon_ImageBrush_Rect_Rgba32_Car_rect.png | 4 ++-- ...ygon_ImageBrush_Rect_Rgba32_ducky_rect.png | 4 ++-- .../FillPolygon_ImageBrush_Rgba32_Car.png | 4 ++-- .../FillPolygon_ImageBrush_Rgba32_ducky.png | 4 ++-- .../FillPolygon_Pattern_Rgba32.png | 4 ++-- .../FillPolygon_RectangularPolygon_Rgba32.png | 4 ++-- ...uration_Rgba32_BasicTestPattern100x100.png | 4 ++-- ...sformed_Rgba32_BasicTestPattern100x100.png | 4 ++-- ...lygon_RegularPolygon_V(3)_R(50)_Ang(0).png | 4 ++-- ...on_RegularPolygon_V(3)_R(60)_Ang(-180).png | 4 ++-- ...ygon_RegularPolygon_V(3)_R(60)_Ang(20).png | 4 ++-- ...lygon_RegularPolygon_V(5)_R(70)_Ang(0).png | 4 ++-- ...on_RegularPolygon_V(7)_R(80)_Ang(-180).png | 4 ++-- .../FillPolygon_Solid_Basic_aa0.png | 4 ++-- .../FillPolygon_Solid_Basic_aa16.png | 4 ++-- .../FillPolygon_Solid_Basic_aa8.png | 4 ++-- .../FillPolygon_Solid_Bgr24_Yellow_A1.png | 4 ++-- .../FillPolygon_Solid_Rgba32_White_A0.6.png | 4 ++-- .../FillPolygon_Solid_Rgba32_White_A1.png | 4 ++-- ...ygon_Solid_Rgba32_White_A1_NoAntialias.png | 4 ++-- ...sformed_Rgba32_BasicTestPattern250x350.png | 4 ++-- .../FillPolygon_StarCircle.png | 4 ++-- ...on_StarCircle_AllOperations_Difference.png | 4 ++-- ..._StarCircle_AllOperations_Intersection.png | 4 ++-- ...Polygon_StarCircle_AllOperations_Union.png | 4 ++-- ...llPolygon_StarCircle_AllOperations_Xor.png | 4 ++-- ...entCentersReturnsImage_center(-40,100).png | 4 ++-- ...fferentCentersReturnsImage_center(0,0).png | 4 ++-- ...erentCentersReturnsImage_center(0,100).png | 4 ++-- ...erentCentersReturnsImage_center(100,0).png | 4 ++-- ...entCentersReturnsImage_center(100,100).png | 4 ++-- ...ushWithEqualColorsReturnsUnicolorImage.png | 4 ++-- ...Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | 4 ++-- ...Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | 4 ++-- ...llSweep_Every90Degrees_start(0,end360).png | 4 ++-- ...Sweep_Every90Degrees_start(180,end540).png | 4 ++-- ...Sweep_Every90Degrees_start(270,end630).png | 4 ++-- ...lSweep_Every90Degrees_start(90,end450).png | 4 ++-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- ...Json_Mississippi_LinesScaled_Scale(10).png | 4 ++-- ...oJson_Mississippi_LinesScaled_Scale(3).png | 4 ++-- ...oJson_Mississippi_LinesScaled_Scale(5).png | 4 ++-- ...oJson_Mississippi_Lines_PixelOffset(0).png | 4 ++-- ...on_Mississippi_Lines_PixelOffset(5500).png | 4 ++-- .../LargeGeoJson_States_Fill.png | 4 ++-- ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | 4 ++-- ...ra32_CalliphoraPartial_Yellow-Pink-0.5.png | 4 ++-- ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | 4 ++-- ...ba32_CalliphoraPartial_Yellow-Pink-0.2.png | 4 ++-- ...Rgba32_TestPattern100x100_Red-Blue-0.2.png | 4 ++-- ...Rgba32_TestPattern100x100_Red-Blue-0.6.png | 4 ++-- ...BezierFilledBezier_Rgba32_Blank500x500.png | 4 ++-- ...lledPolygonOpacity_Rgba32_Blank500x500.png | 4 ++-- ...vgPath_Rgba32_Blank100x100_type-arrows.png | 4 ++-- ...erSvgPath_Rgba32_Blank110x50_type-wave.png | 4 ++-- ...derSvgPath_Rgba32_Blank110x70_type-zag.png | 4 ++-- ...SvgPath_Rgba32_Blank500x400_type-bumpy.png | 4 ++-- ..._Rgba32_Blank500x400_type-chopped_oval.png | 4 ++-- ...gPath_Rgba32_Blank500x400_type-pie_big.png | 4 ++-- ...ath_Rgba32_Blank500x400_type-pie_small.png | 4 ++-- ...Path_RepeatedGlyphs_AfterClear_Default.png | 3 +++ ...atedGlyphs_AfterClear_WebGPU_CPURegion.png | 3 +++ ...Glyphs_AfterClear_WebGPU_NativeSurface.png | 3 +++ ...esCoverageCache_RepeatedGlyphs_Default.png | 3 +++ ...eCache_RepeatedGlyphs_WebGPU_CPURegion.png | 3 +++ ...he_RepeatedGlyphs_WebGPU_NativeSurface.png | 3 +++ ...easesPreparedCoverage_DrawText_Default.png | 3 +++ ...aredCoverage_DrawText_WebGPU_CPURegion.png | 3 +++ ...Coverage_DrawText_WebGPU_NativeSurface.png | 3 +++ ...hicsOptions_ImageBrush_Add_Src_Default.png | 3 +++ ...ns_ImageBrush_Add_Src_WebGPU_CPURegion.png | 3 +++ ...mageBrush_Add_Src_WebGPU_NativeSurface.png | 3 +++ ...ons_ImageBrush_Darken_DestAtop_Default.png | 3 +++ ...Brush_Darken_DestAtop_WebGPU_CPURegion.png | 3 +++ ...h_Darken_DestAtop_WebGPU_NativeSurface.png | 3 +++ ...tions_ImageBrush_HardLight_Xor_Default.png | 3 +++ ...geBrush_HardLight_Xor_WebGPU_CPURegion.png | 3 +++ ...ush_HardLight_Xor_WebGPU_NativeSurface.png | 3 +++ ...ions_ImageBrush_Lighten_DestIn_Default.png | 3 +++ ...eBrush_Lighten_DestIn_WebGPU_CPURegion.png | 3 +++ ...sh_Lighten_DestIn_WebGPU_NativeSurface.png | 3 +++ ...ns_ImageBrush_Multiply_SrcAtop_Default.png | 3 +++ ...rush_Multiply_SrcAtop_WebGPU_CPURegion.png | 3 +++ ..._Multiply_SrcAtop_WebGPU_NativeSurface.png | 3 +++ ...ptions_ImageBrush_Normal_Clear_Default.png | 3 +++ ...ageBrush_Normal_Clear_WebGPU_CPURegion.png | 3 +++ ...rush_Normal_Clear_WebGPU_NativeSurface.png | 3 +++ ...ions_ImageBrush_Normal_SrcOver_Default.png | 3 +++ ...eBrush_Normal_SrcOver_WebGPU_CPURegion.png | 3 +++ ...sh_Normal_SrcOver_WebGPU_NativeSurface.png | 3 +++ ...tions_ImageBrush_Overlay_SrcIn_Default.png | 3 +++ ...geBrush_Overlay_SrcIn_WebGPU_CPURegion.png | 3 +++ ...ush_Overlay_SrcIn_WebGPU_NativeSurface.png | 3 +++ ...ons_ImageBrush_Screen_DestOver_Default.png | 3 +++ ...Brush_Screen_DestOver_WebGPU_CPURegion.png | 3 +++ ...h_Screen_DestOver_WebGPU_NativeSurface.png | 3 +++ ...ns_ImageBrush_Subtract_DestOut_Default.png | 3 +++ ...rush_Subtract_DestOut_WebGPU_CPURegion.png | 3 +++ ..._Subtract_DestOut_WebGPU_NativeSurface.png | 3 +++ ...hicsOptions_SolidBrush_Add_Src_Default.png | 3 +++ ...ns_SolidBrush_Add_Src_WebGPU_CPURegion.png | 3 +++ ...olidBrush_Add_Src_WebGPU_NativeSurface.png | 3 +++ ...ons_SolidBrush_Darken_DestAtop_Default.png | 3 +++ ...Brush_Darken_DestAtop_WebGPU_CPURegion.png | 3 +++ ...h_Darken_DestAtop_WebGPU_NativeSurface.png | 3 +++ ...tions_SolidBrush_HardLight_Xor_Default.png | 3 +++ ...idBrush_HardLight_Xor_WebGPU_CPURegion.png | 3 +++ ...ush_HardLight_Xor_WebGPU_NativeSurface.png | 3 +++ ...ions_SolidBrush_Lighten_DestIn_Default.png | 3 +++ ...dBrush_Lighten_DestIn_WebGPU_CPURegion.png | 3 +++ ...sh_Lighten_DestIn_WebGPU_NativeSurface.png | 3 +++ ...ns_SolidBrush_Multiply_SrcAtop_Default.png | 3 +++ ...rush_Multiply_SrcAtop_WebGPU_CPURegion.png | 3 +++ ..._Multiply_SrcAtop_WebGPU_NativeSurface.png | 3 +++ ...ptions_SolidBrush_Normal_Clear_Default.png | 3 +++ ...lidBrush_Normal_Clear_WebGPU_CPURegion.png | 3 +++ ...rush_Normal_Clear_WebGPU_NativeSurface.png | 3 +++ ...ions_SolidBrush_Normal_SrcOver_Default.png | 3 +++ ...dBrush_Normal_SrcOver_WebGPU_CPURegion.png | 3 +++ ...sh_Normal_SrcOver_WebGPU_NativeSurface.png | 3 +++ ...tions_SolidBrush_Overlay_SrcIn_Default.png | 3 +++ ...idBrush_Overlay_SrcIn_WebGPU_CPURegion.png | 3 +++ ...ush_Overlay_SrcIn_WebGPU_NativeSurface.png | 3 +++ ...ons_SolidBrush_Screen_DestOver_Default.png | 3 +++ ...Brush_Screen_DestOver_WebGPU_CPURegion.png | 3 +++ ...h_Screen_DestOver_WebGPU_NativeSurface.png | 3 +++ ...ns_SolidBrush_Subtract_DestOut_Default.png | 3 +++ ...rush_Subtract_DestOut_WebGPU_CPURegion.png | 3 +++ ..._Subtract_DestOut_WebGPU_NativeSurface.png | 3 +++ ...aultOutput_FillPath_ImageBrush_Default.png | 3 +++ ...t_FillPath_ImageBrush_WebGPU_CPURegion.png | 3 +++ ...llPath_ImageBrush_WebGPU_NativeSurface.png | 3 +++ ...FillPath_NonZeroNestedContours_Default.png | 3 +++ ...NonZeroNestedContours_WebGPU_CPURegion.png | 3 +++ ...eroNestedContours_WebGPU_NativeSurface.png | 3 +++ ..._MatchesDefaultOutput_FillPath_Default.png | 3 +++ ...efaultOutput_FillPath_WebGPU_CPURegion.png | 3 +++ ...ltOutput_FillPath_WebGPU_NativeSurface.png | 3 +++ ...h_NativeSurfaceSubregionParity_Default.png | 3 +++ ...urfaceSubregionParity_WebGPU_CPURegion.png | 3 +++ ...ceSubregionParity_WebGPU_NativeSurface.png | 3 +++ ...t_FillPath_NativeSurfaceParity_Default.png | 3 +++ ...h_NativeSurfaceParity_WebGPU_CPURegion.png | 3 +++ ...tiveSurfaceParity_WebGPU_NativeSurface.png | 3 +++ ...d_MatchesDefaultOutput_Process_Default.png | 3 +++ ...DefaultOutput_Process_WebGPU_CPURegion.png | 3 +++ ...ultOutput_Process_WebGPU_NativeSurface.png | 3 +++ ...d300x300_(255,255,255,255)_scale-0.003.png | 4 ++-- ...lid300x300_(255,255,255,255)_scale-0.3.png | 4 ++-- ...lid300x300_(255,255,255,255)_scale-0.7.png | 4 ++-- ...Solid300x300_(255,255,255,255)_scale-1.png | 4 ++-- ...Solid300x300_(255,255,255,255)_scale-3.png | 4 ++-- ...Rgba32_Solid2084x2084_(138,43,226,255).png | 4 ++-- ...d492x360_(255,255,255,255)_ColrV1-draw.png | 4 ++-- ...d492x360_(255,255,255,255)_ColrV1-fill.png | 4 ++-- ...olid492x360_(255,255,255,255)_Svg-draw.png | 4 ++-- ...olid492x360_(255,255,255,255)_Svg-fill.png | 4 ++-- ...pendPixelType_Solid10x10_(0,0,255,255).png | 4 ++-- ...tThrow_Rgba32_Solid10x10_(0,0,255,255).png | 4 ++-- .../00.png | Bin 13806 -> 129 bytes .../01.png | Bin 14835 -> 129 bytes .../02.png | Bin 13501 -> 129 bytes .../03.png | Bin 14027 -> 129 bytes .../04.png | Bin 13733 -> 129 bytes ...ernImages_Rgba32_BasicTestPattern20x10.png | 4 ++-- ...ernImages_Rgba32_BasicTestPattern49x17.png | 4 ++-- ...rnImages_Rgba32_BasicTestPattern50x100.png | 4 ++-- .../Use_WithFileCollection_Argb32_F.png | 4 ++-- .../Use_WithFileCollection_Argb32_test8.png | 4 ++-- .../Use_WithFileCollection_Rgba32_F.png | 4 ++-- .../Use_WithFileCollection_Rgba32_test8.png | 4 ++-- ...tPatternImages_Rgba32_TestPattern49x20.png | 4 ++-- 649 files changed, 1389 insertions(+), 1112 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index d944ca614..914525ea8 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -874,6 +874,22 @@ private static void DebugSaveBackendTriplet( $"{testName}_WebGPU_NativeSurface", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + defaultImage.CompareToReferenceOutput( + provider, + $"{testName}_Default", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + cpuRegionImage.CompareToReferenceOutput( + provider, + $"{testName}_WebGPU_CPURegion", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + nativeSurfaceImage.CompareToReferenceOutput( + provider, + $"{testName}_WebGPU_NativeSurface", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } private static void AssertBackendTripletSimilarity( diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png index 46017f65f..b24c34725 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:701d483e03920b9a1b9b3b5ddea095118574b7c0716a1d89a6cf5afd86dc6d04 -size 12048 +oid sha256:d28b153b714f7097f51f269295fa3e625be5958f117ce5017ac602a94ab8cbb2 +size 10930 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png index d68fe1bdc..33d3f8ef0 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40b3fbb49e7db2057ddb8c47b4ae5714b7d91314572c8e9406f7c39b4ff13146 -size 3402 +oid sha256:41d684c2a171f8a0f633a00e4eb960458af2daf5ad981cda63fafbfa1c6c88d9 +size 2114 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png index 24acb55da..efb2a587d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2350af6f9f632cad14619e397fea7b8ae14cb6f2f6a03d430e096e322a69d231 -size 13870 +oid sha256:900a4c73a62edb0df9c11cfe1ab81d55532e175d81148f3682cc5c38e1ea46f9 +size 12352 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png index d65188782..9433427aa 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9392c829544595e4fd94f9555151d80bbd2b1b3f21651cea5c3d7a255eabaa43 -size 23306 +oid sha256:5e075f71a20f3fb8957b2412820eb533715ee3968d46a6454c9713b3f0d4641f +size 10939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png index b13d694de..5b46fff4a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7f4076fd9e235fd64c6ac1078787b44738fafbe92dfc7ab57ae1ee4995b8d19 -size 23309 +oid sha256:084c39dc74b3cc84d16b057e785fa6576a09bac3aee87437b3de10c7b4f99fd4 +size 10939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png index 874e0d50e..1ea69c2f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a96553abab4cf7ad4d5c533472f52d0357f961c9eba56cdd182d6664f46abd2 -size 13645 +oid sha256:489da8aa1349a55de5086019a88357114a6ff1576038cdf975894d6648f4b7fa +size 11081 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png index 84033c93b..e836e72ea 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7267c13d86d482f4dfe9def6f884a11ea2cc7878d0cb8dd7774a2cae6191e83f -size 2805 +oid sha256:431a0e81f68c1052900a104702e139051df38cf2aace11df421e695dae7a1679 +size 627 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png index d7764d164..08a73286d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb35fc9756721ea6cd3c997df0b890fb728c72b5e44595481b02b769ecabfdbe -size 10869 +oid sha256:b422cdaad5f46da741f9516d93425653f4ad28c34a3741f15194cc7b45298f56 +size 9137 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png index a9af24829..b75804d8b 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2a57a23b6b4de5739698a9af36d65431222452a0e9e6c404916863e69c01bf0 -size 12411 +oid sha256:641d8788a235efcca45ba2936e04cf8efd0440704b308f0a0b2e094d9aff59dc +size 11044 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png index cf690043b..5c4c1774b 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d9673fef71cbcb6a6eda3e6d89e1716e302efde9bbc1fe9ecb1dd6f30e7eb03 -size 24071 +oid sha256:a0fa52d4c98829843624056b400d33b3c0e61101ef441eadabf78d76e0c02b13 +size 21210 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png index f747c34a2..8b6f68e53 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:796675139385d990ee507b85fea4326fa0d9338a1f56846cb0d43c52e0733b72 -size 28833 +oid sha256:66ffc518934398824ef7762397afc0dea7d7ae227f041c6f560c5da7e76e0d51 +size 26117 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png index 1af1d92d8..e7a5d8d13 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ece6df5e1054b3cffd18b4efa34e98cabcae94fd50327091cd570f671c378b9d -size 5352 +oid sha256:1eb7f23ea51edd63746d319b68fc3608b4894af394c0328761914c3838efd38f +size 3198 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png index 47e1947bf..d50eac278 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b307e9da1ba763506f474fbc94c5f2bc91a7363c8d5f91bc21d6c8d1518e5a92 -size 50388 +oid sha256:a14fce97ebad01b5a5978ef1f475387407357d299a3e641494e03351babb302c +size 45437 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png index 353dd3271..d3e5b6e7a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fcd3c21bf085435a8deedbdc6ebc1a53ee0d8772e7f338fe62b3aeb025324f7 -size 7571 +oid sha256:f021ad48a06ffd4d107bae867746e299561607425762a8402c871c83fdb8f968 +size 3836 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png index 7c504510b..701efcbf9 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff8de261ad72b6d70edb7c9a0e44eff7ed5108d32bbc162aa86a48b4f83851a2 -size 3831 +oid sha256:250654dfafbfe77e1bde33eef9cc9ed15d1c482aaf6794ba868eeb3b13c4587a +size 3458 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png index 0f94e9962..05629b290 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c01d2904133f8a24f6b57517ed9f1df6a9bf9f21d39c52502cb2817f5f79ec15 -size 13259 +oid sha256:d4d703eefd1e0c88bc6bd952382260179f513d2f9c65c9b0ef25943ae8d1e6b2 +size 11158 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png index 9c1c6a7a5..9d052627a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5639df5a84e3a9731982af35325391bbb7ab24b5add9e45e29a6fad055bf8315 -size 2991 +oid sha256:85c01d37cb482890dc5049408be8150ec5f6b64cf818fb138cd6332d0d714473 +size 2711 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png index 312bf9c42..d8416d670 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d23d9f940f0a91b16cc77b6a728c13240d18b654818e471ae65df0ba3666e83 -size 10415 +oid sha256:53747387140e5016d3369aa46400c4214c99b2368421d1023bebb1dc8479b031 +size 8267 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png index ec1d63e71..9aa616d5c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a376eee88cf42ca5c76ac36fac5f123602113bb9c3c7cc565b9e16112727a2a7 -size 23632 +oid sha256:630f6e24deb0eb71d222a9f5f588ffd70cb01e3343cc7172e72ee566b054d7b7 +size 18965 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png index 6a980231d..096f34c82 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d -size 19991 +oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e +size 12939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png index 6a980231d..096f34c82 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d -size 19991 +oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e +size 12939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png index 6a980231d..096f34c82 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92057fe68e9fe50ceaf35ff315af7cce8082febc79ca121be6691eaf57e6c2d -size 19991 +oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e +size 12939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png index b46e34bc6..5787f6dcb 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ac5d46871737d28c60a7d9d71fd074fa2e548b3d3223990b31c2c3f21555d6f -size 6138 +oid sha256:1685e8d64846f15e8f88f2e9e3e82628f8c08792e4b3beff32b566b6dccca8ba +size 4875 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png index 189e5a0e6..812ec0e58 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e81ca1ff7c5f39a6fa517f0a46c1cf986f9569d6f361668c177d56765c61f4ca -size 2650 +oid sha256:152c8529b7e299e6024c33a769a0171846d8e09ce8756be2fe509d17e26f87d2 +size 1342 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png index 7bb7d2865..1c35d0541 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7aa5d86ba1b20fa742505b0c8575b693ee81bbb07244304e54c8c3486e9168b -size 371 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png index 6b10adb50..0f1587481 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 -size 1754 +oid sha256:6fed923205689afec9dee09f05dcad85a8a0ed7f074a0156fb09f1b0db0f9d97 +size 1284 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png index 761b4c89d..ca9a13ab4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:122e92acee8ae979556bdafc6787d98f207bcf2f0fbc7a69a88e4c3b5e65207b -size 1751 +oid sha256:c3ec1e24fa198c5440c3ff374c3033a80438cfbbf59cf00255c4e3f3c21f5095 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png index 6b10adb50..0f1587481 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 -size 1754 +oid sha256:6fed923205689afec9dee09f05dcad85a8a0ed7f074a0156fb09f1b0db0f9d97 +size 1284 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png index 6b10adb50..0f1587481 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 -size 1754 +oid sha256:6fed923205689afec9dee09f05dcad85a8a0ed7f074a0156fb09f1b0db0f9d97 +size 1284 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png index 6b10adb50..0f1587481 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b097e0873960ffe99aafba38e557db85e1a9f30654c33ab435d8757acb1ab4 -size 1754 +oid sha256:6fed923205689afec9dee09f05dcad85a8a0ed7f074a0156fb09f1b0db0f9d97 +size 1284 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png index f7603337f..d1ac5ac4e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed914df1df4be3d1dad56417b78716bcb7c1b118240eb956cb92413257c66393 -size 1929 +oid sha256:6417e4c95ade9ed30d48d52ce3cc02967017aaf9d098fc954c02f40481c7a33f +size 1253 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png index e116823ee..953f328ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9fcfc14ba94554772a0f6cc8f26705c58b87c85f9e16c1f2d5f0e291a38336 -size 780 +oid sha256:7977e99a1b19dad1ab36fc4265712f2e2c19aadce01d39a34b4e40b4386a02e5 +size 529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png index e6eab74aa..79cd7f67a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f242c8e580f6c7ae4c66e42177eb9e95b5fa1f5968a268d71d90b31982351c02 -size 1287 +oid sha256:664959de13475f003cf820c21d0ceb9cc7cb35537641810d684dbc0924e68e47 +size 896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png index 63d19c050..30e91831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5d5bc0280d9612cb7413495cea75bce7288501d74fa1504b0a651894b732b2 -size 1935 +oid sha256:7a4987695e61cda63c354e491a16c785446acfa27423adb4689e5a96a22aae85 +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png index f7603337f..d1ac5ac4e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed914df1df4be3d1dad56417b78716bcb7c1b118240eb956cb92413257c66393 -size 1929 +oid sha256:6417e4c95ade9ed30d48d52ce3cc02967017aaf9d098fc954c02f40481c7a33f +size 1253 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png index 0848b7596..9083163df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896acf8e067dddcbad3df6591218e532d272ea187e599af9723a0f77e5e02637 -size 1491 +oid sha256:af96a68f392a22c4c24cb3e5bf52a38ab12d1eef7d6d74796eb51bb0581b7602 +size 944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png index ee46aaf5d..cb140a042 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718b48b74be41a39615b52ec89adfff3d9afc947034a1a3d86211007102c78eb -size 1686 +oid sha256:e5654c591507b8ffd7b9ec3b8a05fedcaf5c1ad000adf96bc78dbb12dacf0e55 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png index 9dea11fe7..3fca4d707 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a43d0111135bd603978fccf769d91a710bb4f7cd406eb4d86ea230927107e990 -size 1954 +oid sha256:e03cc34c4190df1db19fe2da5b79c00e190ddcd9f2cd44f8b35881101c0e8cd5 +size 1368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png index da321f0cf..5273f1f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55ada515cfb4cae6af46a035f8177a5bdcd1d750a2ae48bce83a36cac38eefe1 -size 715 +oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png index 97f49f609..95f4e5c4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af64607a45f192f73bd48a86787276397b9fae4ca72c5ed2f3bc1b054fb0b33 -size 519 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png index 084ff1bcf..2754dfdb6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c36ad06ee6b1f2e1cd527f25f707beb3cec0d32bc87dc1e527b9a4844268b5 -size 756 +oid sha256:fb1cd6693a9359d352b3991f0df2bfe13ce1b905b843fb22956646d7d0c541f4 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png index 22d5914fc..f605fcda3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a -size 1624 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png index aaaa001c3..35b66a733 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 -size 1623 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png index 09881b5ee..b5724b84b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f -size 1623 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png index 111778ee8..76673a63e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 -size 1625 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png index 2a0453172..9d2e596e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 -size 1626 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png index cc67c91c6..1ab310b89 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 -size 1626 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png index f30bc174e..e55378a70 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3570b7e310568379e9e66e35929de9d2932444d8a2add05141ba072b1d08bda5 -size 760 +oid sha256:b06af1e79bd0bd98b638bd2cd6cc80a25613723d8c7143abcce95e0b801b089d +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png index 1c6c67d9a..20e8f35a9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f48589e6282a0e0c43685d72ba2ce1ec23de54b3e1dfad07089e173098528be -size 760 +oid sha256:c8d16b2057686a61d4be59dbffc052cc03c328c36fcff65ad5ad2b53529d0dbf +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png index cb9431bb2..9ab6cc778 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2659722e58d9506e9469bf8d11bef52dbbb85734c378557a5b35bc9a1f85983 -size 757 +oid sha256:45283aa3e74d93b384df71feeac379a3911582fabdc0145e94a6eb488afe4d7b +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png index e94068a00..2205dcba1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f268d005f5118998bb5c9ca7b382fc09ea2fe0738952256736b2bdf4919e44e6 -size 757 +oid sha256:8500bb6fa35ef940302d9aa9d391077af78f8c509c93900f191e2e080b308d7f +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png index aa07708ea..1c55a5890 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a08118f15161a1ba8905a8b9918b3badefe52cab38e4b868006451746a444f1 -size 762 +oid sha256:fdeac033f313e0151bd177678c697499f8f09c31df65d4e3a9b512f369c90da5 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png index 1b1953c54..adef2c3ae 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2271351e9a8106eab04b74b2909885620ba8c2f141cf40208133c955344a96b -size 755 +oid sha256:5a26c4fb100ccb1026def8738856423e945cb193d8cd1c93365a08f4b3d6dad3 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png index 442db9eb7..9d5057939 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:680dad1c97bf7189831fa9ad0ec97b4e4fc3bda82da2f09e4388754c497bd09e -size 694 +oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png index 22d5914fc..f605fcda3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a -size 1624 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png index 111778ee8..76673a63e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 -size 1625 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png index 09881b5ee..b5724b84b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f -size 1623 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png index aaaa001c3..35b66a733 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 -size 1623 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png index 2a0453172..9d2e596e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 -size 1626 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png index b524d9dc8..2d42e2a0f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d6dd72ffd135a849125c3938f84e3355020b5e88508e9aa66ebaa746ea1cbc4 -size 1622 +oid sha256:6d378094f7ac82529a962ab155ec020c10c51394728f340bf20af17bf6e9bf07 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png index b8535b73b..17360edc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b17a17e838f1f32c310470e94d97aa88a741ab593b180430b19ae2dc8abca75 -size 1524 +oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 +size 312 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png index 22d5914fc..f605fcda3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a -size 1624 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png index aaaa001c3..35b66a733 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 -size 1623 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png index 09881b5ee..b5724b84b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f -size 1623 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png index 111778ee8..76673a63e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 -size 1625 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png index 2a0453172..9d2e596e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 -size 1626 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png index cc67c91c6..1ab310b89 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 -size 1626 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png index cbfd01277..c05007ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce197e2eac9abe1caaf5dc6e770464a0086fc6828773890ce15bf2c314d835ee -size 678 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png index 22d5914fc..f605fcda3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a -size 1624 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png index aaaa001c3..35b66a733 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 -size 1623 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png index 09881b5ee..b5724b84b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f -size 1623 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png index 111778ee8..76673a63e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 -size 1625 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png index 2a0453172..9d2e596e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 -size 1626 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png index cc67c91c6..1ab310b89 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24921e6d1307cc4702df369478bc2ddccdbe87d714a1def3201dbb1ac77ee881 -size 1626 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png index f30bc174e..e55378a70 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3570b7e310568379e9e66e35929de9d2932444d8a2add05141ba072b1d08bda5 -size 760 +oid sha256:b06af1e79bd0bd98b638bd2cd6cc80a25613723d8c7143abcce95e0b801b089d +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png index bb11e6217..7333d02c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb91f87073e5c42daf4487e4fe0b4391fb3eabee4080b1c54d435a7eb1213ec -size 519 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png index 1c6c67d9a..20e8f35a9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f48589e6282a0e0c43685d72ba2ce1ec23de54b3e1dfad07089e173098528be -size 760 +oid sha256:c8d16b2057686a61d4be59dbffc052cc03c328c36fcff65ad5ad2b53529d0dbf +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png index cb9431bb2..9ab6cc778 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2659722e58d9506e9469bf8d11bef52dbbb85734c378557a5b35bc9a1f85983 -size 757 +oid sha256:45283aa3e74d93b384df71feeac379a3911582fabdc0145e94a6eb488afe4d7b +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png index e94068a00..2205dcba1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f268d005f5118998bb5c9ca7b382fc09ea2fe0738952256736b2bdf4919e44e6 -size 757 +oid sha256:8500bb6fa35ef940302d9aa9d391077af78f8c509c93900f191e2e080b308d7f +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png index aa07708ea..1c55a5890 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a08118f15161a1ba8905a8b9918b3badefe52cab38e4b868006451746a444f1 -size 762 +oid sha256:fdeac033f313e0151bd177678c697499f8f09c31df65d4e3a9b512f369c90da5 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png index 1b1953c54..adef2c3ae 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2271351e9a8106eab04b74b2909885620ba8c2f141cf40208133c955344a96b -size 755 +oid sha256:5a26c4fb100ccb1026def8738856423e945cb193d8cd1c93365a08f4b3d6dad3 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png index 0ee00c746..f59ea17c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c99587f5a197945cd3db95fe4050d4a6ffcf35d5cb9e78f49be321cdad28893 -size 762 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png index 22d5914fc..f605fcda3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130ff9352f6cde8f6876803059939568a29e28786d7597c33070e73e2dfbb70a -size 1624 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png index 02c032247..49fcf0c57 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63ad284ed80f3ef0c82171102819f3967d8165a44accc7cf498ed0a871c3892f -size 1388 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png index 111778ee8..76673a63e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a7a239d6c4f22840ec02e31bf639f41fed6b2b084294e96b274495314032d29 -size 1625 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png index 09881b5ee..b5724b84b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca28ffb2850a39176a05a4cd6953733e21795d36a4baf19832baae350142602f -size 1623 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png index aaaa001c3..35b66a733 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a51a1eca354baf2c0d288f39b614b245c3d46e14236541c84f99f298724c6b7 -size 1623 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png index 2a0453172..9d2e596e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f682e47eef27f5fc6379f04edef763a12348ea79bf96cb0e8cef1a8cb91ba0 -size 1626 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png index b524d9dc8..2d42e2a0f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d6dd72ffd135a849125c3938f84e3355020b5e88508e9aa66ebaa746ea1cbc4 -size 1622 +oid sha256:6d378094f7ac82529a962ab155ec020c10c51394728f340bf20af17bf6e9bf07 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png index 8f661c054..d61a0f208 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dc5a0614519298a481c5a62a6acc85fa5bbdf6b2e0014dc71d68e614fbbf21 -size 1620 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png index 50c109be4..3f7a7ae6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b13d406f31958235c6c533802e2e2e40389c0da6b9d4f512515b73f528aa874 -size 1552 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png index 420694cd5..9db4dd1db 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaf1b1667add47bb8198f715dde96d703ede6932176515fcf0a677ebc1faac2c -size 15973 +oid sha256:d78831cd59a95bea191c986ec931d251e6e7243b393a759239c43b632443267a +size 4988 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png index f664fcd7d..483091b77 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6139908f4611be49fbc2a71b8fa601377c7868a000c947c16f8201df76dfe54a -size 14353 +oid sha256:9d3593b23fc0f52360731271313e444175efbbe5a3fe9df0e01422bb66cd311d +size 4906 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png index c12ab5603..95806e725 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0365bb7b03c33109f297808d875379b445aedc06050b331a2dea646c088e2f17 -size 32974 +oid sha256:fe68e33222e02c38133a6555ec7aab8775ddac52e43e65ca08b9642587725237 +size 14318 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png index 3de5762ff..62efbae2d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d8155ab8e62e55cdb2e1800cc1e5c9b2bcce859b7f768830c0c599817f066a3 -size 39354 +oid sha256:d0c0f7ebf2bbb452f8e93691ff62316a116f92aa7a7e8eb0190d277a8130ec99 +size 13195 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png index 7ead6c600..eadea1090 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbc4ff3438ca3f8aeba35886eb560818e69d5df494f11e7906834dcc827c0e30 -size 28075 +oid sha256:a49fb2b4eed39b98932e66c105061981d56bc8d4edcfe0397be99d1538a5acc2 +size 11079 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png index e072cda80..5a7d0917a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d104f6f6956e305c79d61685f2033e31095b6ca8a08d00c28d0e0209826b650 -size 20779 +oid sha256:1bb4baf2bde0ef826e7723c10682c382ddf0919d79c37a3645131e609a65e586 +size 4482 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png index 6424b742c..6489d53c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:456489c36d291f490650d75bbc02940df274a9739aa57b0dbcb0200613568ba8 -size 5878 +oid sha256:ffd79c62b337bc1df02c3f243f63553bd9efce838a4a8f110995f943124dcefa +size 2591 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png index 30e9f56d0..a8cc5540f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c0082e83d7a7d7f6e3ae3d08ec9bb93e56b540e73fd5a2e55203067d174d9fd -size 6039 +oid sha256:3421a0f879544e215c4266b111596c83868168872e10303534761c455bd03b12 +size 2501 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png index 969d80f9b..56687b97a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:790a9e156bee55ddb3d40dd743eafa2a4b0129c43618fea3e99ffd875bd1d551 -size 39092 +oid sha256:45c9a5afb6180d0ba667ace64841566fce63c27e8a537c8fffd2286682f08687 +size 28546 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png index be3036ed9..0a6844498 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79010cd787cfa5828251f46ef014d8e536387d13a99f52ae93c27536546b2b26 -size 5338 +oid sha256:6ac8568a8cd6b0480b541b74a06d9a23d57ecb88a1f761ed84ac3ac01628c2e7 +size 3674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png index f3165177f..87bd10fe5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ec0bb5cb536ac9384d5dc00485557c4fb7e485ab2eabaebf8f3ed290ebbfc8b -size 6657 +oid sha256:6bb79d9722ae69e357a40793034a552976e16083d3ec70cb0d59975e1a90781c +size 5004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png index d330434d9..3a0c39b2a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7f4e06b5e41aefcaed4266c0a978beb2b508da15a1607d9fa0fbd08dd69a4ea -size 7002 +oid sha256:e5f0e9be167df587af31e95fb0738f15128a191947199fdc614be15230658862 +size 5356 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png index c555afc97..4b8e518d4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75c788a96bdba11d1e957ae1267e4bb5a1c3bd84c4eeba0cab9ec5b98066a87f -size 7032 +oid sha256:53e0d07c4f930c7ada67b7648501cb7518e09724fd9605716f5231d6e6821961 +size 5401 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png index 3c11beb38..3d61682c4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8739bdb6ac8a50dcdafd1c8ce4e506659345c377e69af5aae57ba8007b91b837 -size 4591 +oid sha256:bd217c38b95baedd42064b696d975805120d91561c8d77248b749d35c1fbcf75 +size 2315 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png index eaff6def2..ba487fd74 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8cd1828f46fad17c8845c894ee076b6e2c606fae979014d929f97f11643223d -size 6662 +oid sha256:eaa586690cc2b6f379863af5f5e8cf1566a5146d77167dd90e2b5741529cc99f +size 4499 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png index 2aadc6ef1..60af1f394 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beb6bd5f88e1dbbfa1a5ef27a3133891250aa7ad0f49522c8e9acea6fbaf339d -size 8936 +oid sha256:213c26fc13a8f5faffdea4da6892c88c47d63b693a3ccd4187863b83869dbea8 +size 8195 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png index dade34494..7f1c0cb07 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f4e27ede09125901954ef4ed489b7e2e44db93c9b15d2cfb4683a85dcf91b58 -size 7416 +oid sha256:e86050c55b152072eba15794a2409d7a2dff176679eb44ec73baa6744da5b1d0 +size 6124 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png index 84836faab..9a7f7901f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e685adf5fdb7809d25f0d9915cffc7d7e583ec890b4381c789062113f3fc54d -size 6431 +oid sha256:57bd54dc3d42753e9d866785d8efa8ec0a79398de26325913973b005d40cd387 +size 4139 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png index b3859ce0b..368f44ff6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:459dcb4b0e81dc7e850babc169510ac2298636c7a65c06802f104dca3ce87a45 -size 165 +oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png index a10f7de61..368f44ff6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d2ab1c8fd901a5bede28a3036a4df9cb551e6a13c154a988da1700c89fb67b4 -size 161 +oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png index f4d76fb04..b213ccca7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:635053b4ce23dafc080d4d9de9e808819d264461ee5e0543acfefb8c9a04d00e -size 187 +oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b +size 92 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png index ce158e063..b213ccca7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5482537fd9d8f4ad4c6e03ae1ae0d7f8035ecb29f541778009653ab7796510dd -size 173 +oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b +size 92 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png index 1e09fdf4c..a37ebfa78 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1df7aeef7150b0522594a6f2c4d061ed8bc3b328e0ad9059147b6aafe37d8458 -size 387 +oid sha256:f9164f2c53d94e344122f458c2c3d31f5bb1f0aae9f88dc003bb6fd07b827904 +size 209 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png index 17f5c20a8..1b997c824 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb923de979eb0fa90bc91fd53896e3f5868f91ed566fe10213cc77963a936a5 -size 16000 +oid sha256:c5a77a50279300c53b00dd01518c20b7ae08fcd8b1ecf567b2d6a6be4e6dbf28 +size 7723 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png index 886c50592..4101cdaf3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:986152391ad5a528022b98441694d7412637c55367f86ef720816c6d2f9ad712 -size 16925 +oid sha256:09d7232b67122a42a196638c9a8064c6e8deccc771ff52cb3e8be10b1cb99639 +size 14978 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png index 2789f97c1..e8f8954e4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd479f524fad24de0bf9aeee4025563d4df65e428f34bd7960b28f60ca839f43 -size 16011 +oid sha256:5d9a8d0f6639a4f9cb1af2e3de0b9b7ba4829c5d2857682ce928ef81b669ea42 +size 14549 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png index 1f446a1a9..033d08f8c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3f56677500ac0556b5c11591c2f5a56f5f78fd1c2e9c4fb9f1f0962099451d5 -size 14817 +oid sha256:ee87393ba69032cdf05002c9f7bdb9a74831b4c42e2afd38e8308e50fd23eaf8 +size 7361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png index 8ad912ce3..7f6128e54 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa8c0b20f592bfcc49f680bf890a1d007f0ecad26c466129a004b71e515c2827 -size 15689 +oid sha256:03f1eddf5f7f4b7244a1652211a8a271b8641aa25b5bb6306877194edd8e0f4d +size 7996 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png index 1c9bc57a3..0632d49bb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc6d3d1bae5c465b013ee557bb49049704e8d846aaf75f1b343feaa022075e63 -size 1131 +oid sha256:74d9e27ef56c1783e335739185abc8163f7930f20d84605099045bd2ac1cbd0a +size 601 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png index 09abafc72..fdcb3ce72 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79fa696362eb25aaf21309907b7c98c1c64832faee486259e6d418ffc00d2fa7 -size 6172 +oid sha256:0c0bee3610654a496f70379229e4c920f2d18d9d8c9830bb1e2ad287fbb18aa7 +size 3889 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png index 0705678b8..2bbf451ed 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:775bb255b740a368140b86dbd935f564a439f875afb5850e11b95f25283b4fa2 -size 5781 +oid sha256:d8be397a2c3ea3aeee259dc407633f0bf3f6146acda86a1d7bd8e75f4ffa42b7 +size 3492 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png index f2128189a..4dbc02b7c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7328c3c12a80516f4b40c9f102ba173aacff94de56a52b4b0f0eb7e5e3869e36 -size 5781 +oid sha256:cb78c94d064a48e523af4b950507ee0fa7158a7eaa29529dbfdc8676d4c5f35c +size 3901 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png index 5b98c71a1..fb1965988 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:959b77169b16defae22856eb71fdeff006a9592102785fdbe3aef65c70682fbb -size 4311 +oid sha256:ba9da410ee320f2de0f95a9b37abb1d9306a19e6e6e50ad8ada02766dbcc78bc +size 1264 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png index 1f5ff2754..05eea0f68 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83a8335815b3c9f436c85581d3291740e32c99ce1a361bcacf487ee669aaef4c -size 10520 +oid sha256:de873a4abd145eb0aa200df4bccf7b43dcf48a97c97aaa7397b3459b23535eab +size 8823 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png index 5c9d0cae6..57d0f71c6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8a99b608cade5e4188d38fc59c9e79be8e24c568f15b58ebb0f01b08c5e2d50 -size 903 +oid sha256:75b97ff54f46fe7eaa83b2ba7d10d0194a9b9c7f92ccc0d1a5f585befccdfad1 +size 683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png index 5cac37f4c..b47df4405 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea9d64bd82b8210eb9d516fc4123da8bcbe7fc04020f0920f72bda956b7a83b0 -size 2068 +oid sha256:239197be7095b593ed26dac74b25a3c898525380b3bad867d5ea02dd9bbfb9d8 +size 1416 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png index ae5f235f1..1941534cc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 -size 2140 +oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 +size 1544 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png index 1ae34ed28..494270cdd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50a287f41774d0465ea2177e3c87eaa309f8df5b3a58aee6331149e5b26ab989 -size 2371 +oid sha256:eaeb64517972ae4779e416495995f1786d09172adce7e3f0ea54b7865d7ea4b4 +size 1751 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png index 0c599000b..a0734ee4e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:951590c849ed03705712af8af70d0f59ffb55a8e48b45c4dc278259ec15f2ca3 -size 2593 +oid sha256:99e64479ca8316fda5b758b8a0a7e25f67bddc8704e525af8085b843e693b7c9 +size 1998 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png index 2f38a9502..4ea595dff 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21c39fa0aa7a52eda377fc25089fd62470eef10691be5bbaf4ca72cf89f573de -size 2767 +oid sha256:0d60ad285866d2042734cae2a0ce2d47035ff74d545604ff2491140cfc55cc59 +size 2275 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png index 5c9d0cae6..57d0f71c6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8a99b608cade5e4188d38fc59c9e79be8e24c568f15b58ebb0f01b08c5e2d50 -size 903 +oid sha256:75b97ff54f46fe7eaa83b2ba7d10d0194a9b9c7f92ccc0d1a5f585befccdfad1 +size 683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png index aad3872b8..50fc63296 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa202f9f65d214a7dc1dc61f07d08d135d53884c9772542b9babcbfbdfc06ea2 -size 1359 +oid sha256:428bb82e650ec3bc35540fbb03d3da3ee976e18bb2bcee8a022ce9de9be3ea0c +size 965 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png index 8a06ad3bd..bb7c6e05b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c10895ea30d5e72e99edaedf2044fce105575f80b58b058b5430fc931dccd2e -size 1384 +oid sha256:de3d0a7937b6afb5996a00df048e3cf0799e09a7aeda53dadaa14f534d486aed +size 812 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png index 1abf93c62..d6383b786 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e7e786bde0d54ab5b0192cdcc03e1932c40683ed91fb247690cb88ac812fc09 -size 696 +oid sha256:6f5baea6da8377f4a01bc57de4619c737a9650b7907076dfa9ebd0bc8ca0a97f +size 542 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png index 2ca11b59c..cdfea92db 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87469226e962a3ff3db25caf1dc5707631c01839e9ef984577f2ad081119a1e9 -size 1952 +oid sha256:2a0684defec9733bc839cd55ebfd446782a714a643acddabc331b2d0e7382b86 +size 1580 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png index 4470c5792..9c9a12a7f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7b85450deb90ed831cdcdf34d31313d4cd037c7935acda748a3c2ef454ba5da -size 2010 +oid sha256:a7a41dd06dbdcb2c519640df53d268d6c6c4498484e0b5eecb28b7a18d8394a1 +size 1412 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png index 5cac37f4c..b47df4405 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea9d64bd82b8210eb9d516fc4123da8bcbe7fc04020f0920f72bda956b7a83b0 -size 2068 +oid sha256:239197be7095b593ed26dac74b25a3c898525380b3bad867d5ea02dd9bbfb9d8 +size 1416 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png index e0b5752cd..21eeb3737 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1d29f46f25b6894ea5eeb9776e6ac49f6864b09fc19eb25e6276615aa98cc99 -size 2338 +oid sha256:7ce285d7df77e1ce59de7590d993b1f29b5822504ec1caa2a30a4af080a6c522 +size 1683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png index d3c717eb6..62a24e749 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d299c25acdc26626b31630b137b8bf335c609253563ba996921e38acad58c9f4 -size 2211 +oid sha256:9f793714c9049cb99946a2e0fb0581bff65ba62533ef75b99baf34d75131670a +size 1760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png index 71ac13755..996b694a2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be605c4896f739c0c412ca72e6985a8765dcbdbc1278618ef7ef987bb9d6afbe -size 1902 +oid sha256:0ec85374ced15e9af4fe6598c1a4f380a8c2726b3f1d65981ee3047ac6518f87 +size 1310 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png index ae5f235f1..1941534cc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 -size 2140 +oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 +size 1544 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png index 15a946552..3dcb40ed3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2624abda9a217e5613ef26af7cbf18b8bd2f67c83b406a4cce58c1f7e54f12a -size 2060 +oid sha256:2424ada6ae6672f808b666d0b603cebc912a17f620ac86d88ae94b8e73839ad9 +size 1482 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png index 97603b562..fa021944a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43882d13eb2e3360a9e1ec771c31c10eaba820e09e76e7f073c69552bc09d41c -size 2229 +oid sha256:fa55f990bca716f98eee8675fca9536c821dcd1944b229d469f93104ebca4a2f +size 1589 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png index ae5f235f1..1941534cc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ef22ea1bb98cd90d1d93cae35aa1ed5d85e3524eaa48185e9b76fd191dafe7 -size 2140 +oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 +size 1544 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png index 1fd9d9708..5510cbb77 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22fcdd48ddadb352d00032f9fc44076e5aad73964ea481860b6d45cfe848836c -size 118 +oid sha256:cb27d43cc9608027f87b0b9dfb56404a3c6a7f5de3a86746836bdf1756b01559 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png index ce3f363d5..f6793dc92 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f79a9486aee6b3201ba20876028c25b1251e70996af8e4ee4847ac294e87458b -size 59884 +oid sha256:a772b2e9f117174a54de856c3a9b3ad0e592b6c45d56f0f7930cafe424367370 +size 24695 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png index 71a0bc8ff..c3a129338 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20c4f6324712fcc2e6b6cf012c169290a01a9199eb96ba3691550b75b2b2b524 -size 150296 +oid sha256:b3aa1aac6aa2484bf7eae2e6fd08de4c7b6110833e19e7ff4b217e005192933e +size 100593 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png index a0fe867a5..74465715e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb1807440c0a8abb05eb332b379dabff4b1be83f804cc3e2e65978a758373940 -size 48551 +oid sha256:2c5046daca9f61c66a91e323c2762fa6eb86bcc0a75430d34acacb774319af95 +size 18999 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png index 5acd7f8fe..1fe5826ba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b3e455c552537815ca1d5c0699b5fa36bd1963a0a28a06c8cfcf5fb8c5c884f -size 251984 +oid sha256:5267aef9d7cf7f6adef2b84332a814aa8891ecf9060d142cec534d586b3ca55e +size 73970 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png index be90717e5..1fe5826ba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fd4fd80d6bfb522b21d884fc6aba6bb5049d5feeb5a05c8b86e087a7229a440 -size 299061 +oid sha256:5267aef9d7cf7f6adef2b84332a814aa8891ecf9060d142cec534d586b3ca55e +size 73970 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png index 7f87f9d47..109bad9cf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0a67021dca36099ae77be86b20481a60d483f565a9dcfa698bdbb9fb3926849 -size 30112 +oid sha256:64f79e92ad86f3efb9cdbafe15c7fffb04694362d1d5cceaa5ea613c68a940f4 +size 28900 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png index 7f87f9d47..109bad9cf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0a67021dca36099ae77be86b20481a60d483f565a9dcfa698bdbb9fb3926849 -size 30112 +oid sha256:64f79e92ad86f3efb9cdbafe15c7fffb04694362d1d5cceaa5ea613c68a940f4 +size 28900 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png index 1909adfc3..bcb3f3844 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc7901f3fbb29f3addd593406aafb867ba4c852629ad87ccc80f6e83181c4e44 -size 4609 +oid sha256:da374c22ddaefda0d975a5c50f3efc3738e000314a73c878ad9dbf1455f47e8b +size 4433 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png index ed9c7271d..1999238d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0adfeb7a17cc4261701d41646216423c6cf066c0023cbd6b57571539fb333d83 -size 7641 +oid sha256:03880367b9a9a528f01b4d0a398d48ed069ad1e3f1d3cae7519e298f121b3307 +size 7037 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png index 6fb508f10..2989aa05e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:009a9e8035f3d1926bf7e25ed563564aec080e17472fd963df8d857f53233507 -size 7701 +oid sha256:2e0ce379320f6b2b225d8d0c26e2e956aa484341b0dac43f8523e51361a56cd1 +size 6978 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png index 7eb5bc6d6..348ab784a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6160847478f64b019c0bdf32395b8700df0052313f35bfe952ad3b48c6671e9d -size 7634 +oid sha256:19bd1e4651295d02b7635609a029e236a1c2c5a1db96c8191f87fcaf69c4ac0b +size 6858 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png index 382323e55..714585d94 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17c185a74dcde400c8e65585582669747eeeefd59e5af2322a62dd6c471a28f9 -size 92774 +oid sha256:cbebd336f68bb4232d683ed6ae5e8659cd274ed5ac3c72d41b9e0935c5bd3139 +size 4674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png index ee0dd5704..8dda79f32 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 -size 130 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png index ee0dd5704..8dda79f32 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 -size 130 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png index ee0dd5704..8dda79f32 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6be715e71dcb9582c9fbbd1071af39edea1cdb29120074c67e526a5e61aaa22 -size 130 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png index f2d7da91b..86978ce00 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f77b69a6935c2a3813777cef931cb9e1d735849baa9e56d75771f7493ecae76 -size 169 +oid sha256:ff0a58b52f199e1c96255546fae91c3eadc9055786848ec061fe637c94346350 +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png index dddebf3da..42b48642c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78bcdde736e2b0ff90a07405d596ba9f5dac7d4af67deb6f907d9117e79cbc96 -size 189 +oid sha256:0ddf6564ea8a8c8c7c8e919cb046b82ed5e7932ea8c2b4a1b23f5ee3914463ea +size 151 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png index 34978f1ef..72d55008e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2c97732c0f92c328522b352fb81ec0671fc395b25d7d5ab9e2c336b040a819 -size 181 +oid sha256:652c64d699abdcf15f858be4479e4837a0310ee3749028fb46a340e736c4080f +size 145 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png index c512f35cb..ba25a6969 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a87b74dd9a488e759864e75de78a23ba74ed93108027f839119e85583a5a74 -size 175 +oid sha256:69c58759a0fb50eb92431e835a8ac2fc7a97032bbc88f321c56832e6b6271f49 +size 148 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png index 5f7077b41..d62d7aecd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4ae9cbe72c9f83368a38edb158b373648ff92503c815ddea14bc434d60d1659 -size 217 +oid sha256:3187dbe8408a733123fe1a15f4245002858c70586d0fb1f31f9bdaa9035bee27 +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png index c1be2838a..3d75bf3e0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3ff0b6c04c39f5b1399e4781dede08176af01abfa16b7968dc11bce88beca09 -size 329 +oid sha256:fd27868ebc84bc9614f78540f96ace5082dd4e5f2deacf22d684a9aa58835a5a +size 116 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png index 1348e7857..1ffbe1b0e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c866ee869ea057b4fccf70ec9fe9f91da9a67e1dcd0d40636fae4107a799e9f5 -size 324 +oid sha256:0e9edc0787ab2997d19da66c0b59d836c8e0acd9422834a01f60d35de37a2d15 +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png index c024c4904..842796a9c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9d31234277dce14a01fea910aba703ef78982fa038d32ec56a2e0409ca4f1ff -size 319 +oid sha256:f49d7730c8e8c52f2b072bbe2ff2717464aad4eebc14ec0bda20596e08e7f7a5 +size 109 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png index 1fd9d9708..5510cbb77 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22fcdd48ddadb352d00032f9fc44076e5aad73964ea481860b6d45cfe848836c -size 118 +oid sha256:cb27d43cc9608027f87b0b9dfb56404a3c6a7f5de3a86746836bdf1756b01559 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png index f93de56a2..cf385db2f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d631cd560df9f95e9e5bf19794c78b7f1598c0d0d80498991208d412ff4c88c3 -size 153 +oid sha256:5511d05de6c5b7de1db013eb2dafb1b869ec353377829a89785c0ef3a4d5e41f +size 97 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png index 28c0a0bca..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4c448f01c00b5e5be518c14a20e9d7c01853d47a58c791e8790738424f9e82 -size 141 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png index 4d9445bc6..30cec9964 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5d99aa86b5a87bdf2c893cd4d50747eedc8a85661b0ddbf38c7e027c2de0f73 -size 9163 +oid sha256:14841d28a0bc5218d7b5d969d990a6df51757b77b0065f19bae2263aec70e1c0 +size 2783 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png index 2588ada26..9026bab6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9f72a01beefd90d9f82aca8654ae3001ccb39a66b5b4498a07996a988c74bd0 -size 384 +oid sha256:33d6a4d77f6d9418dac470876aa6aa2c5bd274e6107b93579daf14f90cbfa854 +size 136 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png index 2588ada26..9026bab6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9f72a01beefd90d9f82aca8654ae3001ccb39a66b5b4498a07996a988c74bd0 -size 384 +oid sha256:33d6a4d77f6d9418dac470876aa6aa2c5bd274e6107b93579daf14f90cbfa854 +size 136 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png index 738f02c6a..ae3660f5d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d0f7f9f291aca84d38ed3abdad5669bf82be1cb8f540f6a88bd8f0c10bc7aa5 -size 793 +oid sha256:9808622aa1a16df85ec3911f95072815a4f1cada6fdb10ed89ad79d732edecfb +size 332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png index 48b5a132a..ffe949b9e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adc584e03db96076ed34a4f38b39352a39e4d215a8f780a5bcafd214525ce063 -size 788 +oid sha256:19d71f16b40889bbd18033a1f591d6530a5c4aa0f0ffcd7b2da882c720d35b9b +size 368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png index 48b5a132a..ffe949b9e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adc584e03db96076ed34a4f38b39352a39e4d215a8f780a5bcafd214525ce063 -size 788 +oid sha256:19d71f16b40889bbd18033a1f591d6530a5c4aa0f0ffcd7b2da882c720d35b9b +size 368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png index 738f02c6a..ae3660f5d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d0f7f9f291aca84d38ed3abdad5669bf82be1cb8f540f6a88bd8f0c10bc7aa5 -size 793 +oid sha256:9808622aa1a16df85ec3911f95072815a4f1cada6fdb10ed89ad79d732edecfb +size 332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png index c86a8c7f6..8d4a81f75 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:612da10c5b7b402d71afbc84f7a712fe6371a7e36edaa564a369b60943ae4aed -size 397 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png index c86a8c7f6..8d4a81f75 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:612da10c5b7b402d71afbc84f7a712fe6371a7e36edaa564a369b60943ae4aed -size 397 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png index bfa7f3be0..772cbc724 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa5112662ee94a8fbee6c8e911bb513840993b3a3a43a346adb2dcace5b0e4be -size 334 +oid sha256:8a7e58de9fec685980aecd0811d72c3ff37d15899ea8d9a216da589336f5f627 +size 186 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png index bfa7f3be0..772cbc724 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa5112662ee94a8fbee6c8e911bb513840993b3a3a43a346adb2dcace5b0e4be -size 334 +oid sha256:8a7e58de9fec685980aecd0811d72c3ff37d15899ea8d9a216da589336f5f627 +size 186 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png index b6abcd266..435275e21 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b51ecc4260b3680bd22e42d4b5ecb84ecd37854d7654c05e58532ec2d715a82b -size 218 +oid sha256:aeeeac9e3bf6a5b633ffb537bcde7133017d56776c727450633c1df7dc6de737 +size 163 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png index b6abcd266..435275e21 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b51ecc4260b3680bd22e42d4b5ecb84ecd37854d7654c05e58532ec2d715a82b -size 218 +oid sha256:aeeeac9e3bf6a5b633ffb537bcde7133017d56776c727450633c1df7dc6de737 +size 163 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png index bb6ada2b9..cc8710ec3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f0a9dd18ec12fbd464b370907bbeecbda5c31adebbe8ca8dbd2b98b8b0d01a3 -size 174 +oid sha256:25f45451fe5c6898611cdd7504d5f68a419e3fe8e2614cc5da1b0022b6a8864e +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png index a372ec97b..f7d162409 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c90be6cdc577df1fcaab8d689c552a8ce0b04e7d56ddf129f6cc0418f2c5cf48 -size 168 +oid sha256:01608bed44a5e936807fb77b44ba1d2f4bccd84efd0774d29145775521a90892 +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png index 68dea434b..bd35802bd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7c69d3ae3db0921b127c7858feee727692c02740b17ec147b59fa33d86fb776 -size 176 +oid sha256:0693514c8034ecc07a6eba1971b7a35343226d757fd66603c9e4d09864747b8a +size 102 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png index 38361a376..5af54eab3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7858ae798df8188dfee00652f00ec6f50b01ad4ae220a4d0d0a557adcc2c15f -size 179 +oid sha256:23882cab09040361405de161753c0ffe2f27f6d3160495edf3ebff25cc4ca4b8 +size 102 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png index d7037317f..3823ec42f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81294e07eb5da65cb00944b6462c5c93d368e936cb38cdbf61a4930d21bb58be -size 184 +oid sha256:87eeebbfd7a24863bf73aa42b544528f7928ff7dc80d698f7650aafa28487d85 +size 93 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png index e9c92bb55..66efecdea 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3ae1449dc56c7f8d460591cfb52d32220090809d24c2e28e58d251c1dce0b2a -size 180 +oid sha256:87b01b762fa99c54d5a294640344e559768b049e9d1954e2ccd5205f9fb82126 +size 93 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png index 50bca02ea..bb9d648d9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bebc805cc88d273bc0da8c1df5fc3a75cfaaf42fe0143130090cf988d218f574 -size 181 +oid sha256:b813035f11b0d3abc80360ad38ced07ec0961d0744e438c13650bf76185dfdb1 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png index 501ddf10a..2ce615085 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b209e3c08cde62127a97dd0c64bc3a0e6168d2803dfa2f16d809d671c567e1bd -size 177 +oid sha256:b1eced1a6acf836a5d8916585c13087c987d6c7ec070d020e2347dd06cdb33ae +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png index f334102c4..8fa401b3a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86271e033299427d6ca6eb294abf248fc5b0567a9a32632d4e187e61c52ad700 -size 198 +oid sha256:595a11891dca2657e14cc2b906b810f6d6089f00f552193cc597066c5b5de43d +size 99 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png index 9823b0c1e..731c7682f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1d1d9d948ff823aa2da07b43140b763d35aa144bc053cc62f45305904bbc8d7 -size 198 +oid sha256:6d69e73b2793eef700ac9bea010e8f40d9c588ebd34428d4bb2505b3ebe91190 +size 99 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png index 428df318c..96553c88f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43d0bfe675fd1d4a142a164d3ad87dddfde0835b38a2ce4ab5bd602a7d8787b8 -size 167 +oid sha256:a34da0f8310f7d90c8d571d08502794f984d63d74f24e2e0e26a2bc1768b0315 +size 94 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png index 7274161be..fbca36055 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ca932784c29726f0f1e519f74c1b59fe107e5ba7cad4146eedf0eb23b81cec2 -size 166 +oid sha256:80c73db0b7f4e6b76a56bcb893cfa65f7da0a113751c505d0b0953618bc1763e +size 94 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png index d86b89596..dc2a9cabc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2f6a6f4d29f0aad8189679c12d5109ab0844d63d14231f4bce89db2ad97689 -size 164 +oid sha256:f4c4ecd5a396025b3868859e2ddfcbdd7943b2494bf0b31c35d467097abccf96 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png index 197e0a11e..4e26095c1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10585c68d57efbaa3faecb141f84fdfc9629415bec1591e1ee236afb7df6ecda -size 180 +oid sha256:c624408bb46eacdd6795a4d6738315a457d74f6b3ee8ad24c87802fd58da1029 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png index 666fead72..d029a873a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a -size 753 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png index 666fead72..d029a873a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a -size 753 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png index 666fead72..d029a873a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a -size 753 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png index 666fead72..d029a873a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74f51520ae7e591656a25d1e67b281650965089dc9aafda41fd6d8ae54e4f53a -size 753 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png index 0f374410e..b5a207f2c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f095e7c1a78e5ae92c053aa4b1bc3b2469486f6650fb21dc0957b55ba852137 -size 1710 +oid sha256:931c4ccc31543101fdd1196b8784ee939b643477b4272213988d95ef47efe30d +size 223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png index 0f374410e..b5a207f2c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f095e7c1a78e5ae92c053aa4b1bc3b2469486f6650fb21dc0957b55ba852137 -size 1710 +oid sha256:931c4ccc31543101fdd1196b8784ee939b643477b4272213988d95ef47efe30d +size 223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png index 9f0037f8a..85bc516d9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb -size 3078 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png index 9f0037f8a..85bc516d9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb -size 3078 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png index 9f0037f8a..85bc516d9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb -size 3078 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png index 9f0037f8a..85bc516d9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaaa384ed6514006f99fa8be332531cdca8915f300ccfe917b114bd9821c9cb -size 3078 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png index c878a2d63..317d43265 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3e5dd912b09cec622f2c751016d687c3b7fa8edfb4387176675c0bc4f5df49e -size 48115 +oid sha256:39252d1bc31ac8ffca3e4975f87a8d15197a47e17506f5c4c857a6327db011ca +size 38416 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png index c7c963e0f..0945fc432 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b8d3817418cb2eb0700975763f143c3a23db87e32e294836b409080c7fcdf75 -size 30251 +oid sha256:4721c27c827c3f716ad18ae1cafc32132ae47b6557a01d4c16acaa7b7d92400b +size 20601 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png index fa919bbd6..41994df52 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69cb1264e59b110ec51afeeeeb3f24d5c314c4b3c9d6b1c1221ba066a6d22afc -size 16292 +oid sha256:662e4f78996d9c12d6698410543c6ef0aa474337b2d03084924b1c706a1e4916 +size 13462 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png index 5a7223563..d24bd6b86 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec49fc14608b92be4b2f11bb5988ba36e0e7b55ccdaab6643757ac66fa7c56df -size 25167 +oid sha256:24f74782275e63f241431410af079089f2bd229ab06071a472de1a360ca934d7 +size 17093 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png index 8ab2e0750..ad5b15cb6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2bb73046097700066001f12d682aee690cd8e82e8c0844195381b9337703711 -size 3219 +oid sha256:9b08fd85ee90ab448f31ba0eb686142e93cd8d80e0cc6b8abb3a9f615bd5e5cb +size 1687 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png index 81a10570f..43bc50298 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c69974c77c9265f9af9eb93afa38ac741b4f7ed56aa0766d10070691ed13ce02 -size 1925 +oid sha256:82278e1de2e0079b3db5f1d9da3939c725081dd7d791d3e9cef2ef9de0f7aef7 +size 268 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png index ab6221927..7a4342074 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9500a506cf668803bc42bdc34fecbfaa3210ddc5f49f05e0ce94228668a0788a -size 866 +oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e +size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png index ab6221927..7a4342074 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9500a506cf668803bc42bdc34fecbfaa3210ddc5f49f05e0ce94228668a0788a -size 866 +oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e +size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png index ce8a6b4e4..15431f30a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:024dab2c1b0a56059148e8e1bbd9f3e9deabbde452f2edc6350d8b81cf115f12 -size 2484 +oid sha256:02891cbdc2242395343290bc5403fd161fab49e470f93fd0c5639a93464f274b +size 1754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png index b54d96c7b..4e29cc25b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86fb466875c45eca3a2d781934aea6773d1ffae6c885e6934af41de6e4b1ebca -size 2574 +oid sha256:f95be339c0fc7f9315968001722777d1eebddbf6eea41c8d9d524b8775842763 +size 2024 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png index 61319dd4e..3fe215ed7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:981e3fac81d73c81dd2f7d9a410902f20dd73edc68af533aed35aa0f1aee1e96 -size 3001 +oid sha256:86280439d207ed0a74595757af265a313d75f7ff8b7502eb17d2d7184855eb12 +size 2499 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png index 722981dbb..8ad422f6d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:279afb7b9ad0144bda9d0d9eb5b19dbe29ef2969ea5821dc9e6473b6ff9ffec8 -size 3196 +oid sha256:d762246aeec860558a8ee8e5318126937be11a9561a4bd1ff18f90900858bc2b +size 2852 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png index 5080ee1f1..c7cb00188 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4ed7f179e3be4fa87c2196927ad9f32798a25a6aa05e22eae5c00eefa00e36c -size 3521 +oid sha256:20457c79f2f5a782088bc4f9a333d0092ba9c5f5307835cdfc3ca7bf527420e4 +size 3247 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png index fbb6127cb..5a79a3c77 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:381e1c8eba1a3970e6f5c01f841bcf77a200ca501982fec759c381cd3c708ddb -size 154 +oid sha256:245519d3fcf3760d2f18b27e7e6f2e93b2c45bb9a544c66886cbe16c7fd669a8 +size 97 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png index 66151f9b3..8fdea2ff0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:774e1d9632c69630c151288b6b637d046ad72d57a3017929344c17ceb2d5c621 -size 160 +oid sha256:0c5ab8ba6a6a320eb7b5add844061a8f24aee9cce0eb51488c13cac70615bd44 +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png index 66151f9b3..8fdea2ff0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:774e1d9632c69630c151288b6b637d046ad72d57a3017929344c17ceb2d5c621 -size 160 +oid sha256:0c5ab8ba6a6a320eb7b5add844061a8f24aee9cce0eb51488c13cac70615bd44 +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png index a12766270..12ecc129c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b210f8412d1be85464b7038f8b92a360347473598016aebdbc90e40e4effba28 -size 4690 +oid sha256:b81ef6b2a5b4f848d740f1ff9de08c90c0dc1e7ef3d11b43c2b704b29546c26d +size 2806 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png index a4287c105..d6c24cbf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89ec9b036a0e9e685e97fea67f1efc2ec310ffc8956aa78ddcb462b3cc22b366 -size 4874 +oid sha256:145da3c382448615cf0b90f4718b8a79a8b2d7f3dd33042c755e15d5b127d33b +size 2788 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png index 46efe9db1..7c67a4ece 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92fc9b26b79772356b9bae39b9d8903add17da7a483018947614426c6b693de5 -size 4680 +oid sha256:779a0d18611eb44bead0f9dbd704c3c9e10fbf92e906fdd4281075f3d5c946f9 +size 2906 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png index d39599e36..7cd33f335 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b0c338157679f1e84ebfcb4e0f113e699e9157e67522d4a6d9d19f681b4fed5 -size 3377 +oid sha256:a07709ddcd6eebe041998bd62e8186be0aeac91e6f4e7da19ca3ea247344270b +size 975 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png index c10fe1374..5be0df234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c85cf1e911143fa2d4f54a8bc8922021d5961b96d257137cfa21f2e5aec00eb2 -size 6808 +oid sha256:ac469abbc75f28cfb40ff8dc84879c6a5a92b0259ae87cd475e3c2df48b2cbbd +size 5421 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png index 83cca9694..debdd636d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:634e8133e16ae16f66bad2e95b109d99af8b82dc32a8b4f9e23af81282d18922 -size 2083 +oid sha256:5f1615fbe0f7e8a309d26da49eb1d62661b66b1f5dccc20766d539ca406ee946 +size 1010 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png index d78c4a208..dde4f7412 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d7bcb3676267de117078593f60bfa4ce675a2c07a5cb0979bb9faf80a2f4afc -size 3618 +oid sha256:22a71b4f18cae498e33ba68c137f8329d903f59976296040890fb4395f8b56f5 +size 2854 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png index 2136151b7..283497551 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d74bc3ac9a9b374e769473abaa652bb0976db262b5e409ab9da8021c3bfd4edb -size 3738 +oid sha256:3d1456250f5dd7a33d8719363f718ebc414bcce7459cb9bf17780f79f0a1e313 +size 2940 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png index 5420e25e2..37f181075 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1902d6dedd1b8ee99ed7f6617b8226559c1c4dca9b76b52f90fe7882a00edf21 -size 2455 +oid sha256:309caf3e21c0b0cc0748d270cc6f40e2507a4b297996155adba75981c39feda7 +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png index 36e469dc1..a6628377d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9138adc1bdbc42b7542300bbf9e7bf518de10e9f2e5e145be33135ca2cc41eb -size 3617 +oid sha256:963e72b72ee771576aa3af1c2da943efb9b63e45f370f990717a8b472eac73ff +size 2855 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png index c2c164204..50e6559e0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:682aa4e681c9887ffd50b5bf70f2d91f0bf3cc7de880e55ff914a490faf2cc6b -size 4081 +oid sha256:c92da609a4c66a3775d65fded2472d3ed3a72c5af1fde8c1148e19d0f3b36346 +size 2134 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png index 88b563c84..c6d01e640 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4b4b963eee4836ccd4d2dee013eddf5c7974f5510786d8cba0652ff00de2860 -size 5121 +oid sha256:0b39304d8ce4df9a5f741f6336486b27e78cf80de07d3cb5fd56d9d2b75c1afd +size 1912 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png index 8b11acddb..ccd515477 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:539aed2d3addb82ac61ebc3b4f8d52dd4f45487b63d295ffc89b71cfb8bfd4c3 -size 7371 +oid sha256:4a30fff36046c0b58859f102bcc22b8ff90fd2c19ee71a8814b1c14c1b0032f3 +size 3355 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png index ae98e58ee..29f91d6ed 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cf1cb768b3e325d4554f4a0ccfcb48baf932423c7cdd80295cd0f5b65e0b042 -size 9177 +oid sha256:4d27da50076609e7c215fa566e09d88ca98abef487d742870801a056c126d306 +size 3021 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png index 2cf1f58ba..b6b082da6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7ae5f9ece34e2d7b85f3095a82864fa203c921c01fc3b33f7d794fe5126c376 -size 13547 +oid sha256:7657e38e341c96ede6e80e929eb1d22dafa04f19692a929a1cf1f4d535a0d889 +size 5171 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png index 3f00c2339..2234ebc2f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b449799e1802ccd0193bbb8b0ba69a6c589598b9ea6442fa81940c5c68ad4a7 -size 637 +oid sha256:672f2ab9f185757958192d8c28f94a800706a4f1ad3cfadc042443cac04056f1 +size 100 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png index 4fd81df91..cf2790c36 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe8910283272b5fc0bbcbd3ec545ec5bba126a31e8a724f3888b8a0ed38f241 -size 144 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png index bd94e0ba9..7631eab46 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb777fd06bd45b173fc3e93e02a16103f70da85eb6893c7ac2cc92db4ac9a474 -size 143 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png index 0449c3a70..0878f254f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aef8e625d769ce4495aed0bd9f25aa5bf82947ddb51493747d8e89b2a5d60267 -size 24210 +oid sha256:b321256dc4d6b6a12a7ddc7b40151ca7c602e763c88e7170aa056ac48d84b203 +size 10561 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png index 5c94cdb6b..509f92aa2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cdb28418145dd9089bf603c57b2c174507b424ff39453765f13159ae3618033 -size 24332 +oid sha256:daacb570dc72db1e98f2c0f9d7ba6463c4bb0eccb5d91daf7e68f2e5ea049967 +size 10788 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png index abde514b7..3677ec835 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34e77c3b4254a6a59bf17ef396f43f7cdf9b54b1ef6ccc0f6f617bc69f9f73a9 -size 24423 +oid sha256:e4d3158afdf03dd6e8084f26e506fafeb7eaf1e6d257b78446462d92e79a0fa4 +size 10641 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png index 384c6ec1b..ab37337d6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d37683f159db8687bf1d168c749f7f5a02c65aaad7d64daf544458ff45703f4 -size 24168 +oid sha256:5a28966be8af741d960f99575333a6a7a7878782ee241a92f9d9d1bfb7277f50 +size 10436 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index b34e8debb..ea923d341 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec9e8c3a49d3c66fecec0bd653515028d67461d021c9f46dd919f275a943569b -size 31720 +oid sha256:391cb9c926dd579f1aa0ed327e4d3a8509139cdacae4f3b0edc97106f79dd9b2 +size 17378 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 8953a1a8b..2de4910f5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4209841a105ad355efd21e0e023a6f75fa38644f968b99d14e7c12f0ad5e7e5 -size 2822 +oid sha256:691dba96a0bae7fdbd18ec903d842342e3bb76e2ce921bf615f705a3b0d309f7 +size 778 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 70d555da1..28988a333 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea350bbc5712d5ed72d9942343e80e01417481e76274acc116ce379a76a83dba -size 29995 +oid sha256:2183370685492ce65749a6c320ffc821131adb291c12163ebe185a2e2f707965 +size 16823 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index a3c036f79..1c981f991 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b568cca71399dd0184e9096a9b6cb64a19570f459909cf24ba9d73b4c99afc8a -size 28427 +oid sha256:6e44002ae9eb867c91406a82dada3b768cc9aa300f931b511ac602a18c21178f +size 15102 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 19242c796..c7be2c3f7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:418ba58fbfd91d9249a1eaee7333272139988e07850c05c0d2ad54fcc48405ba -size 2407 +oid sha256:c64c88b72b5a1018e46b8234025c4706fc362b746737e6c04dc01e36f15189b0 +size 725 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 2c8dd12ba..85315b4a4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ea2fbe00d35143940184844d193c129e6aeb8e265ae597a805736ca8f89e30 -size 26685 +oid sha256:7bcbf518dc950feeef34927feb3c89068ceb5412ba0bdb0a106e69b723dc8910 +size 15498 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png index c4e08730e..c5a94188e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:677bd21bf95fba0e6258d397e7d9989ad7457c013b04f79bfd4bfb3b93fb5556 -size 235022 +oid sha256:536d6c42b490c383833605478e4a9875b41e01c877136b696b4b5ea204f269ff +size 80945 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png index c4846cc2a..4ba7d7ba5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cccaa9676afb69e361559cd23029c706d69bc78ad719e83b7ad1f99fbfd50110 -size 51543 +oid sha256:cfbd2e1028aed3bab74464d5d29b6c8b06c4181fe374368f0b587439362a073f +size 18146 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png index da2cce583..087e32589 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8308b029f57a6afad50e2c5cc77d8ff725f951e4a5626d8950f9364d04dec868 -size 97573 +oid sha256:36935a802152d9cb8b6b8f26af66ca7485331f0f9cf43eac5c98bc9f64b49c24 +size 34735 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index 79e70fc51..911950d6c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b51ce4dea1dc1a906d8b7b668370c296a47bb60241d8145847b622bf24c92363 -size 12064 +oid sha256:13e5d2a6cb238750401137ee17c9e3ce4f1067218e1466b2ddb03fd3b162ddb0 +size 4486 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index 69f16cb59..5025bff97 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97dd990daefdfcc8b0bab788b4e43c15c3d99f38972787946df28668a59a6d79 -size 162968 +oid sha256:fc551f322b933a876d450c9a1af0e8914f047ab5a471e7f7b4c228d2619e89c5 +size 41042 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png index 87b5da525..a2e70e54b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d63caefa6f9131f14084a3df4a3885b73e8af0b4b3a7aca24e322732f3bb878c -size 456461 +oid sha256:5ab6952916f050467e1caf24f2d8106db7e7778d83ffd13117b5dafd5b2a9106 +size 407761 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png index cebcbc75a..997b6720d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:032c48d0f1b41d73f4200ac7f702b7bb2584f5f76e8255527dd645bb606cc67d -size 361732 +oid sha256:453db170043d15c55302819678b2dd43686388196f9307e254a1e9796e247fae +size 306360 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png index d108058d5..1b7e4c6bf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6231dc4f3124d1093131305988ae3d12606477ac6ec2a0b91c0c15b6d54b93f -size 380198 +oid sha256:608e0ce36b4866c077cb025e5eca78b8e85e4a5b9f2fa4459e97a410f27928cd +size 313445 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png index 498753590..265e0f2be 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2f7a77ed18350bcaa2daa4ad99eef1d3c9a270add4df560c0738ffeaf6ad456 -size 12300 +oid sha256:553b92cb26ffb752d7ada262c7d918c840a6ca8b91f480714ced7a751259e89a +size 3329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png index 612d67db7..925389091 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d82b27a84a4707e98dcb96c6c3e62efc6c45dc6c7a87a2deb1f8f86532b1a5ec -size 388651 +oid sha256:5db7f7974d1d83cea37e88cb7cdb1482563c0264b5dd3b9d3d0f971b8a6c5283 +size 325228 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png index 4bde3c324..c48efa24f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fb52a23ea8aba4e9a5225d801e055881550153f49ca3d14601c16540812b5c3 -size 12111 +oid sha256:b2aadb60a463ce878e6f6d03ab44fa8b1ce1f29f166e2ec9619f50b4dfd00ebe +size 4964 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png index 19c8c2115..e07ff4945 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d73d10abb987d5e633ea56f50180fa38488d2cf9fe7a209ba75b357a65c141b -size 12053 +oid sha256:b85840ba2c6c185bc7f979a9e889aae74d5ae4781cc3915e817105f6a6af235f +size 10085 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png index 4406ac4f3..2ec013d52 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f440475125b34e2f937aab639972e6b030534ed45bd57e0afd9d0a55373b1a55 -size 7216 +oid sha256:2e8d67dbbd4fc8a7f17ed6fe300033e44a050ef2044a3fb6cfd9272c6d55816f +size 3188 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png index 9bc8ba0f0..266a6d6b9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d602f640498fa64a1968d5dd477f422dd197ceaa3696e9d6445a1579cfff824a -size 6966 +oid sha256:989c843ed10a31190d812545fff20bb9fa0aeea67ca0053af31fcdb06aa6d4de +size 3004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png index b06cfb14a..3c27e680d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9452a9f060afd92ec07865020a7bec1ec7bebbe0786fec07ee222e6b1f4da460 -size 860 +oid sha256:4e792c1b683634907b942c45e4693121a77d5f0184e59124f78ed936f131de63 +size 407 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png index 20306876f..9bdb776c9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a87a78717cef0b311f0539d6c308d12cd4d3f71630d5a3ed75f445e9f9ae63d4 -size 963 +oid sha256:649424597ccdbdbfbdf0aeb85c83f232a5970a0a4322d7f1681df6b6ca45ec37 +size 681 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png index 93de85cd5..a06918917 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e578c7d8ffbbb9b55615f94c8c5a552a8ed5039dd31254db173a664b2694b33 -size 902 +oid sha256:a1d8c462a23afc5b2558e3d044c71a21af222ef79101993c0c302dc508d6eb26 +size 486 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png index 45adf807f..ab555b0bc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7dc775d74c97666a4acf55d2ea6e1a2e1759534a8e2a9c0b7adfad9055ef34f -size 9329 +oid sha256:efb39f74cc777f108ab9df20956796cf1568d06d60df8943769bf897993a06ca +size 4887 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png index e0579d33b..c19affb07 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e035406af73431431322375302852d9c6e45d6a7d5a4eef6fd6c50cb733e158d -size 5289 +oid sha256:1a5e03190fa9497ccc55ad3923e7242428d78d533fa0f9dcf8965d06793d68a8 +size 2794 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png index 990cd474a..562c76e6b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36c189c6914e2f52dc41f2f0417ef27df5989d5ec89ffee950cfa31d2466415e -size 5193 +oid sha256:cb899460473490c06e3933b0a872ba6c05256e223896caadcdb462a93807d771 +size 2459 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png index 2c0a3d926..613ac0e05 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf38018dbb981504f346d1da7acf323de3dc19f0c21e8279e5b7be9a5024019f -size 9572 +oid sha256:3d64a09bfa99e9c0e90b1c02c8bbaaa0fa4afa5a92d8988388d4ead6aa769dea +size 4822 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png new file mode 100644 index 000000000..a2a98cf0b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dacc58e79528708bb121de4e70a6fcfcd16749903120419461c292b8abd0569 +size 4694 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png new file mode 100644 index 000000000..09a15b690 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0604ce8a71f1cf5be81da96ab3c8073e8bd15e2f5f18097ae63828f3e1a0d72 +size 4771 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..09a15b690 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0604ce8a71f1cf5be81da96ab3c8073e8bd15e2f5f18097ae63828f3e1a0d72 +size 4771 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png new file mode 100644 index 000000000..dd5a2ace1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f94cb03a84826ff994379c9203937a92dfbe83af80986ded3c567645713c6f6 +size 4890 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png new file mode 100644 index 000000000..70a207546 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:075b21e5fc234edb1fd161e069a34787b1dcdb3f29606e8f0cb0951968fdef49 +size 4825 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png new file mode 100644 index 000000000..70a207546 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:075b21e5fc234edb1fd161e069a34787b1dcdb3f29606e8f0cb0951968fdef49 +size 4825 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png new file mode 100644 index 000000000..39f290334 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81717e72015b32fcffa1a59d931a843b5b1673dc8ffbff638f2490fd009ad180 +size 36496 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png new file mode 100644 index 000000000..7c6d73b75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10da9c5b194281e56877383455f117043cd072fc57e247af7dfa6b42d968d422 +size 36884 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png new file mode 100644 index 000000000..7c6d73b75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10da9c5b194281e56877383455f117043cd072fc57e247af7dfa6b42d968d422 +size 36884 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png new file mode 100644 index 000000000..1a905ff83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 +size 566 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png new file mode 100644 index 000000000..1a905ff83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 +size 566 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1a905ff83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 +size 566 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png new file mode 100644 index 000000000..1b1ed3e3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png new file mode 100644 index 000000000..1b1ed3e3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1b1ed3e3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png new file mode 100644 index 000000000..0d51f838c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png new file mode 100644 index 000000000..0d51f838c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0d51f838c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png new file mode 100644 index 000000000..00a793ec2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png new file mode 100644 index 000000000..00a793ec2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..00a793ec2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png new file mode 100644 index 000000000..443c5e78e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png new file mode 100644 index 000000000..443c5e78e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..443c5e78e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png new file mode 100644 index 000000000..0627f844b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f +size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png new file mode 100644 index 000000000..0627f844b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f +size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0627f844b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f +size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png new file mode 100644 index 000000000..71d06c28f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a +size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png new file mode 100644 index 000000000..71d06c28f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a +size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..71d06c28f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a +size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png new file mode 100644 index 000000000..d8b6ebb18 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 +size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png new file mode 100644 index 000000000..d8b6ebb18 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 +size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d8b6ebb18 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 +size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png new file mode 100644 index 000000000..212ff2e1a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:448effa8e8cac551ffb3f849109f07291e575cbcd0c6e8bb4cc7b8c514772312 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png new file mode 100644 index 000000000..212ff2e1a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:448effa8e8cac551ffb3f849109f07291e575cbcd0c6e8bb4cc7b8c514772312 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png new file mode 100644 index 000000000..212ff2e1a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:448effa8e8cac551ffb3f849109f07291e575cbcd0c6e8bb4cc7b8c514772312 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png new file mode 100644 index 000000000..bcc59e5ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 +size 472 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png new file mode 100644 index 000000000..bcc59e5ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 +size 472 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png new file mode 100644 index 000000000..bcc59e5ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 +size 472 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png new file mode 100644 index 000000000..ff3590331 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png new file mode 100644 index 000000000..ff3590331 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ff3590331 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png new file mode 100644 index 000000000..c561128ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png new file mode 100644 index 000000000..c561128ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c561128ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png new file mode 100644 index 000000000..43394d294 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png new file mode 100644 index 000000000..43394d294 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..43394d294 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png new file mode 100644 index 000000000..31b07cb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 +size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png new file mode 100644 index 000000000..31b07cb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 +size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..31b07cb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 +size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png new file mode 100644 index 000000000..1ad01578b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png new file mode 100644 index 000000000..1ad01578b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1ad01578b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png new file mode 100644 index 000000000..9f6074d7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b +size 474 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png new file mode 100644 index 000000000..9f6074d7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b +size 474 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9f6074d7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b +size 474 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png new file mode 100644 index 000000000..c76cbf48c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90784f2523c8e7d6680cc17e618893ebf040033642a3cbaad73918c2f5d6b2f8 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png new file mode 100644 index 000000000..c76cbf48c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90784f2523c8e7d6680cc17e618893ebf040033642a3cbaad73918c2f5d6b2f8 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c76cbf48c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90784f2523c8e7d6680cc17e618893ebf040033642a3cbaad73918c2f5d6b2f8 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png new file mode 100644 index 000000000..b9ac5f192 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6392bded60931b04bd5044a2e789405506bb8c98f4ac271e04af7698696c929 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png new file mode 100644 index 000000000..b9ac5f192 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6392bded60931b04bd5044a2e789405506bb8c98f4ac271e04af7698696c929 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b9ac5f192 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6392bded60931b04bd5044a2e789405506bb8c98f4ac271e04af7698696c929 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png new file mode 100644 index 000000000..55694d401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74b4e0b213dd604413745b05195fb9bbf5eacac1883ade35b73f4985a800b69b +size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png new file mode 100644 index 000000000..1eeb01770 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3bfb3cb3510c77beb21625d4d45bb3c10629f5469b4b4910d202e71967dce94 +size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1eeb01770 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3bfb3cb3510c77beb21625d4d45bb3c10629f5469b4b4910d202e71967dce94 +size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png new file mode 100644 index 000000000..516e2f405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png new file mode 100644 index 000000000..516e2f405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png new file mode 100644 index 000000000..516e2f405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png new file mode 100644 index 000000000..55a946401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png new file mode 100644 index 000000000..55a946401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..55a946401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png new file mode 100644 index 000000000..096f34c82 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e +size 12939 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png new file mode 100644 index 000000000..367e87dd7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73308cc124098be2c2c84ff4b56009b7031533d543bf9ccb3094349737761fac +size 12907 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png new file mode 100644 index 000000000..367e87dd7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73308cc124098be2c2c84ff4b56009b7031533d543bf9ccb3094349737761fac +size 12907 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index 1770f1516..cf5b2640e 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc86c51ad4946fb8a314c8d869a83cc2496d30468036729c3827c2c121cae69c -size 1068 +oid sha256:1cc025e5fffdbcc7c3b97755e87bb02ceeb837dbc7ca810ab14434b116ac554d +size 106 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index a60e07711..6984eb70f 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f86685f6f2326e629b8b84340bb1b3cbcf6ffe75facff0931b613337772345 -size 3296 +oid sha256:d2e05a237bfd4f5ce3083f3978cedff7c67d4d25ca4a4b2a2edc58d0a46d9444 +size 1988 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index 5bf6378e5..4eb87dfd1 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534d3fece38b94386b6ba20aa121addda1beea61d38cb34b9d2ae09b662fd38b -size 3585 +oid sha256:b471b131f89b3c45c9dbe9a3cc421e00de41f4143890cb84060c86793184b024 +size 2214 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index 61df79c32..9c07514e6 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fff4ab1fec04c432529fd67d9c50934ce083fa6e7c0c4432fd6520eac2e53ac -size 3625 +oid sha256:7c56684bdc6fe4b3cc6b5acfcc400adf4ddd801ca06b587eb7689eb2e4bb857d +size 3132 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index d5630da20..cc11924f9 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92a356847e8aa36b361a2208021a0c0e2a3287d615f0b948bdbcc0bc3b336bc4 -size 4300 +oid sha256:7d9dd0363fe1f9d13b8d27abb812fae6651209b1e6ad3fe54d94e967883326c7 +size 3532 diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 66cb782f5..956f6473a 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ca5183dc6ba28a4455e4b8de50ce9a1a48acbdc964cb06d8b76da8b12c2ed9c -size 140372 +oid sha256:a80eed08bfbf24ab5b9a7503c8751cb8ad476e2e3f5569d405d3e4a8e88bf5b9 +size 116734 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png index a17cb3530..ce83d58cf 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca1adedef43e57412f765d19b4521e064e129a65023bd2bff68963948499e8d -size 37235 +oid sha256:6f8f2d2f9f855e726e8075c400aab4edc2ebd128b539d35f1dff37d4f02669d1 +size 31939 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index 91a2a83c7..9433427aa 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a745434f58765a4f0ff0bf8b85abebe065b66e6e3f2e5f476efdc796270054 -size 22596 +oid sha256:5e075f71a20f3fb8957b2412820eb533715ee3968d46a6454c9713b3f0d4641f +size 10939 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png index fd6287171..c23244fc0 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:397458a75a31312e5c6af70e8d5e006cb4a139a78218d411ed4bc5a8792f35b2 -size 37267 +oid sha256:058edc03c921cbd6fe1da9df82000a453c4d88dcfbd1a5fc756be854c2053f45 +size 31954 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index e050f7ff1..5b46fff4a 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e91666dc1855f8753998c29c97073542737e75f7af0b08aaf1a41060ff4b60 -size 22599 +oid sha256:084c39dc74b3cc84d16b057e785fa6576a09bac3aee87437b3de10c7b4f99fd4 +size 10939 diff --git a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png index 5d808e14f..1927b2a43 100644 --- a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png +++ b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78036e25b5bfb3211e8794ec11d71e385401c8ff569e598a32152e7a8023eac9 -size 118 +oid sha256:2ccaca38823033a6aacde86619c7c959a79e65d36b5e8e94f42b762b47344f10 +size 82 diff --git a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png index 5d808e14f..1927b2a43 100644 --- a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png +++ b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78036e25b5bfb3211e8794ec11d71e385401c8ff569e598a32152e7a8023eac9 -size 118 +oid sha256:2ccaca38823033a6aacde86619c7c959a79e65d36b5e8e94f42b762b47344f10 +size 82 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png index e9052e9f59e8267d7e281af657768560430f167b..9b752b4a7e7ebef929c9b61bbe00c1d11836ca01 100644 GIT binary patch literal 129 zcmWN?K@!3s3;@78uiyigCJ=%C252J8sC0z(;OliSd+}$q{?>KQV{FDe+PpnwEdT47 zm(pKm99+x=s<&N_nvm{W1omVLig2`&eZ<1ZC>dQM1{*mffh4OV!AKb_>^`^{&;xl` L)o5SV9?7a7&MGG1 literal 13806 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 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png index b538aeaec75188f8a1625997435d94d5e53eaf16..71a2732d8acd561f7b192c840dffa723098f26fc 100644 GIT binary patch literal 129 zcmWN`K@!3s3;@78uiyg~43Luk1_%fZN4Lq(fP-MX3Ec-;Ym5>}+rocXX;&cVH!Uzs|1%g^5q05#s MG$rF#HBuz?2eKU}C;$Ke literal 13501 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¥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 MA`0%mBE118{+8P&H~;_u literal 13733 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 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png index 381ff3db9..a57a541ad 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:883a948b8920fc6930f82b4985ab4fdf7d046a508221df88811a646a52036f11 -size 135 +oid sha256:35a69eb51a6954642789a80105a6133877f66c685c1564ae328fd18efceb2009 +size 121 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png index f2d513996..209a13c0b 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e8795374bf53ed7585950437af71cbe6ab3ce5c6fda2c4da35566cb398333ac -size 155 +oid sha256:b541dadd48b1a136fed30442fd57fcb94f554c51f7869c2803e0ac72b98e97dc +size 122 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png index 74de36cb4..a56e0e6d7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:015529debc61dd2eff6b61de0c107e6bdd87eb15bebe45b99f1d38b13242cec9 -size 219 +oid sha256:6ffc4784ea34bc5424363c9a5e9229bb2c1d330c49bac2a324db16c40b2f52b1 +size 132 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png index 464b8f0b0..69e8495e2 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6773e8bfdcd78eea58c95c773a13a76b9e23dd0f16058675ade0d50786437fd1 -size 483 +oid sha256:c66b3009b2527c13e519c6cb9c86733e103a2b719b556c098380244d15a9a9e1 +size 183 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png index 3595cd1cc..81383beb7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf11d42f98951eb78873103c2bc51c690da6ff292e4f4ce25d9e30856b0f61c -size 404 +oid sha256:d3d0639717ff52db04200e00a75cf06490e55a661059511572954bec3d60dce9 +size 384 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png index 464b8f0b0..69e8495e2 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6773e8bfdcd78eea58c95c773a13a76b9e23dd0f16058675ade0d50786437fd1 -size 483 +oid sha256:c66b3009b2527c13e519c6cb9c86733e103a2b719b556c098380244d15a9a9e1 +size 183 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png index 3595cd1cc..81383beb7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf11d42f98951eb78873103c2bc51c690da6ff292e4f4ce25d9e30856b0f61c -size 404 +oid sha256:d3d0639717ff52db04200e00a75cf06490e55a661059511572954bec3d60dce9 +size 384 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png index 211f4f24d..c3ee13817 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:952e58d9b70c65831d8d5c0aa151a7842e859e3519534d60d9e803492262aee6 -size 283 +oid sha256:3f809bac08fea0254309757288583ee82b8f7aa756e126e12b83730482fd8c03 +size 211 From 368f99df3ef8f4ac54c6d2af4a0128fece4cb007 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 17:23:40 +1000 Subject: [PATCH 071/136] Remove legacy APIs --- .../ImageSharpLogo.cs | 72 ---- samples/DrawShapesWithImageSharp/Program.cs | 32 +- .../Extensions/GraphicsOptionsExtensions.cs | 50 --- .../{Processors/Text => }/DrawingOperation.cs | 2 +- .../DrawingOptionsDefaultsExtensions.cs | 15 + .../Processing/Extensions/ClearExtensions.cs | 66 ---- .../Extensions/ClearPathExtensions.cs | 72 ---- .../Extensions/ClearRectangleExtensions.cs | 63 ---- .../Extensions/ClipPathExtensions.cs | 30 -- .../Extensions/DrawBezierExtensions.cs | 102 ------ .../Extensions/DrawLineExtensions.cs | 102 ------ .../DrawPathCollectionExtensions.cs | 107 ------ .../Extensions/DrawPathExtensions.cs | 101 ------ .../Extensions/DrawPolygonExtensions.cs | 102 ------ .../Extensions/DrawRectangleExtensions.cs | 99 ------ .../Extensions/DrawTextExtensions.cs | 248 -------------- .../Processing/Extensions/FillExtensions.cs | 50 --- .../Extensions/FillPathBuilderExtensions.cs | 75 ----- .../FillPathCollectionExtensions.cs | 213 ------------ .../Extensions/FillPathExtensions.cs | 68 ---- .../Extensions/FillPolygonExtensions.cs | 66 ---- .../Extensions/FillRectangleExtensions.cs | 62 ---- .../ProcessWithCanvasExtensions.cs | 2 - .../ProcessWithCanvasProcessor.cs | 2 +- .../ProcessWithCanvasProcessor{TPixel}.cs | 2 +- .../Processors/Drawing/ClearPathProcessor.cs | 46 --- .../Drawing/ClearPathProcessor{TPixel}.cs | 65 ---- .../Processors/Drawing/ClipPathProcessor.cs | 48 --- .../Drawing/ClipPathProcessor{TPixel}.cs | 72 ---- .../Processors/Drawing/DrawPathProcessor.cs | 46 --- .../Drawing/DrawPathProcessor{TPixel}.cs | 43 --- .../Processors/Drawing/FillPathProcessor.cs | 59 ---- .../Drawing/FillPathProcessor{TPixel}.cs | 66 ---- .../Processors/Drawing/FillProcessor.cs | 39 --- .../Drawing/FillProcessor{TPixel}.cs | 38 --- .../Processors/Text/DrawTextProcessor.cs | 70 ---- .../Text/DrawTextProcessor{TPixel}.cs | 36 -- .../RichTextGlyphRenderer.Brushes.cs | 0 .../Text => }/RichTextGlyphRenderer.cs | 0 .../Drawing/DrawBeziers.cs | 52 ++- .../Drawing/DrawPolygon.cs | 39 ++- .../Drawing/DrawText.cs | 3 +- .../Drawing/DrawTextOutline.cs | 37 ++- .../Drawing/DrawTextRepeatedGlyphs.cs | 24 +- .../Drawing/EllipseStressTest.cs | 10 +- .../Drawing/FillPathGradientBrush.cs | 2 +- .../Drawing/FillPolygon.cs | 18 +- .../Drawing/FillRectangle.cs | 20 +- .../Drawing/FillWithPattern.cs | 35 +- .../Drawing/Rounding.cs | 143 -------- .../Drawing/{Paths => }/ComputeLength.cs | 2 +- .../Drawing/DrawingProfilingBenchmarks.cs | 92 ------ .../Drawing/Paths/Clear.cs | 91 ----- .../Drawing/Paths/ClearPath.cs | 96 ------ .../Drawing/Paths/ClearRectangle.cs | 69 ---- .../Drawing/Paths/DrawBezier.cs | 141 -------- .../Drawing/Paths/DrawLine.cs | 137 -------- .../Drawing/Paths/DrawPath.cs | 124 ------- .../Drawing/Paths/DrawPathCollection.cs | 192 ----------- .../Drawing/Paths/DrawPolygon.cs | 138 -------- .../Drawing/Paths/DrawRectangle.cs | 118 ------- .../Drawing/Paths/Fill.cs | 82 ----- .../Drawing/Paths/FillPath.cs | 66 ---- .../Drawing/Paths/FillPathBuilder.cs | 91 ----- .../Drawing/Paths/FillPathCollection.cs | 109 ------ .../Drawing/Paths/FillPolygon.cs | 80 ----- .../Drawing/Paths/FillRectangle.cs | 69 ---- .../Drawing/ProcessWithCanvas.cs | 1 - .../Drawing/Text/DrawText.cs | 181 ---------- .../Processing/FillPathProcessorTests.cs | 310 ------------------ .../Processing/ImageOperationTests.cs | 6 +- 71 files changed, 154 insertions(+), 4855 deletions(-) delete mode 100644 samples/DrawShapesWithImageSharp/ImageSharpLogo.cs delete mode 100644 src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs rename src/ImageSharp.Drawing/Processing/{Processors/Text => }/DrawingOperation.cs (90%) delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs rename src/ImageSharp.Drawing/Processing/{Extensions => }/ProcessWithCanvasExtensions.cs (96%) rename src/ImageSharp.Drawing/Processing/{Processors/Drawing => }/ProcessWithCanvasProcessor.cs (94%) rename src/ImageSharp.Drawing/Processing/{Processors/Drawing => }/ProcessWithCanvasProcessor{TPixel}.cs (95%) delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs rename src/ImageSharp.Drawing/Processing/{Processors/Text => }/RichTextGlyphRenderer.Brushes.cs (100%) rename src/ImageSharp.Drawing/Processing/{Processors/Text => }/RichTextGlyphRenderer.cs (100%) delete mode 100644 tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs rename tests/ImageSharp.Drawing.Tests/Drawing/{Paths => }/ComputeLength.cs (91%) delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs diff --git a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs b/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs deleted file mode 100644 index fd92011bc..000000000 --- a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.Shapes.DrawShapesWithImageSharp; - -public static class ImageSharpLogo -{ - public static void SaveLogo(float size, string path) - { - // the point are based on a 1206x1206 shape so size requires scaling from there - float scalingFactor = size / 1206; - - Vector2 center = new(603); - - // segment whose center of rotation should be - Vector2 segmentOffset = new(301.16968f, 301.16974f); - IPath segment = new Polygon( - new LinearLineSegment(new Vector2(230.54f, 361.0261f), new Vector2(5.8641942f, 361.46031f)), - new CubicBezierLineSegment( - new Vector2(5.8641942f, 361.46031f), - new Vector2(-11.715693f, 259.54052f), - new Vector2(24.441609f, 158.17478f), - new Vector2(78.26f, 97.0461f))).Translate(center - segmentOffset); - - // we need to create 6 of theses all rotated about the center point - List segments = []; - for (int i = 0; i < 6; i++) - { - float angle = i * ((float)Math.PI / 3); - IPath s = segment.Transform(Matrix3x2.CreateRotation(angle, center)); - segments.Add(s); - } - - List colors = - [ - Color.ParseHex("35a849"), - Color.ParseHex("fcee21"), - Color.ParseHex("ed7124"), - Color.ParseHex("cb202d"), - Color.ParseHex("5f2c83"), - Color.ParseHex("085ba7") - ]; - - Matrix3x2 scaler = Matrix3x2.CreateScale(scalingFactor, Vector2.Zero); - - int dimensions = (int)Math.Ceiling(size); - using (Image img = new(dimensions, dimensions)) - { - img.Mutate(i => i.Fill(Color.Black)); - img.Mutate(i => i.Fill(Color.ParseHex("e1e1e1ff"), new EllipsePolygon(center, 600f).Transform(scaler))); - img.Mutate(i => i.Fill(Color.White, new EllipsePolygon(center, 600f - 60).Transform(scaler))); - - for (int s = 0; s < 6; s++) - { - img.Mutate(i => i.Fill(colors[s], segments[s].Transform(scaler))); - } - - img.Mutate(i => i.Fill(Color.FromPixel(new Rgba32(0, 0, 0, 170)), new ComplexPolygon(new EllipsePolygon(center, 161f), new EllipsePolygon(center, 61f)).Transform(scaler))); - - string fullPath = System.IO.Path.GetFullPath(System.IO.Path.Combine("Output", path)); - - img.Save(fullPath); - } - } -} diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index 04497dc93..d5fd9309e 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -21,8 +21,6 @@ public static void Main(string[] args) { OutputClippedRectangle(); OutputStars(); - - ImageSharpLogo.SaveLogo(300, "ImageSharp.png"); } private static void OutputStars() @@ -239,11 +237,12 @@ public static void SaveImage(this IPathCollection collection, params string[] pa int height = (int)(collection.Bounds.Top + collection.Bounds.Bottom); using Image img = new(width, height); - // Fill the canvas background and draw our shape - img.Mutate(i => i.Fill(Color.DarkBlue)); - - // Draw our path collection. - img.Mutate(i => i.Fill(Color.HotPink, collection)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + // Fill the canvas background and draw our shape. + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.HotPink), collection); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); @@ -264,11 +263,15 @@ public static void SaveImageWithPath(this IPathCollection collection, IPath shap using Image img = new(width, height); - // Fill the canvas background and draw our shape - img.Mutate(i => i.Fill(Color.DarkBlue).Fill(Color.White.WithAlpha(.25F), shape)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + // Fill the canvas background and draw our shape. + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(shape, Brushes.Solid(Color.White.WithAlpha(.25F))); - // Draw our path collection. - img.Mutate(i => i.Fill(Color.HotPink, collection)); + // Draw our path collection. + canvas.Fill(Brushes.Solid(Color.HotPink), collection); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); @@ -282,8 +285,11 @@ public static void SaveImage(this IPath shape, int width, int height, params str public static void SaveImage(this IPathCollection shape, int width, int height, params string[] path) { using Image img = new(width, height); - img.Mutate(i => i.Fill(Color.DarkBlue)); - img.Mutate(i => i.Fill(Color.HotPink, shape)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.HotPink), shape); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs deleted file mode 100644 index c6e022560..000000000 --- a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -///

-/// Extensions methods fpor the class. -/// -internal static class GraphicsOptionsExtensions -{ - /// - /// Evaluates if a given SOURCE color can completely replace a BACKDROP color given the current blending and composition settings. - /// - /// The graphics options. - /// The source color. - /// true if the color can be considered opaque - /// - /// Blending and composition is an expensive operation, in some cases, like - /// filling with a solid color, the blending can be avoided by a plain color replacement. - /// This method can be useful for such processors to select the fast path. - /// - public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Color color) - { - if (options.ColorBlendingMode != PixelColorBlendingMode.Normal) - { - return false; - } - - // Only the first two alpha composition enum values can fully replace backdrop - // for an opaque source at full blend amount. - if ((uint)options.AlphaCompositionMode > 1U) - { - return false; - } - - const float opaque = 1f; - - if (options.BlendPercentage != opaque) - { - return false; - } - - if (color.ToScaledVector4().W != opaque) - { - return false; - } - - return true; - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs similarity index 90% rename from src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs rename to src/ImageSharp.Drawing/Processing/DrawingOperation.cs index 4ea3bbf03..d98eac94f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +namespace SixLabors.ImageSharp.Drawing.Processing; internal enum DrawingOperationKind : byte { diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs index 4d4c66737..cd06ee8c4 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs @@ -71,4 +71,19 @@ public static Matrix3x2 GetDrawingTransform(this Configuration configuration) return Matrix3x2.Identity; } + + /// + /// Clones the path graphic options and applies changes required to force clearing. + /// + /// The drawing options to clone + /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set + public static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) + { + GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); + options.ColorBlendingMode = PixelColorBlendingMode.Normal; + options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; + options.BlendPercentage = 1F; + + return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); + } } diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs deleted file mode 100644 index 6813c8fca..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of images without blending. -/// -public static class ClearExtensions -{ - /// - /// Flood fills the image with the specified color without any blending. - /// - /// The source image processing context. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Color color) - => source.Clear(new SolidBrush(color)); - - /// - /// Flood fills the image with the specified color without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, DrawingOptions options, Color color) - => source.Clear(options, new SolidBrush(color)); - - /// - /// Flood fills the image with the specified brush without any blending. - /// - /// The source image processing context. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Brush brush) => - source.Clear(source.GetDrawingOptions(), brush); - - /// - /// Flood fills the image with the specified brush without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, DrawingOptions options, Brush brush) - { - Size size = source.GetCurrentSize(); - return source.Clear(options, brush, new RectangularPolygon(0, 0, size.Width, size.Height)); - } - - /// - /// Clones the path graphic options and applies changes required to force clearing. - /// - /// The drawing options to clone - /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set - internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) - { - GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); - options.ColorBlendingMode = PixelColorBlendingMode.Normal; - options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; - options.BlendPercentage = 1F; - - return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs deleted file mode 100644 index 36505d90f..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of polygon outlines without blending. -/// -public static class ClearPathExtensions -{ - /// - /// Flood fills the image within the provided region defined by an using the specified - /// color without any blending. - /// - /// The source image processing context. - /// The color. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Color color, - IPath region) - => source.Clear(new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an using the specified color - /// without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPath region) - => source.Clear(options, new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an using the specified brush - /// without any blending. - /// - /// The source image processing context. - /// The brush. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Brush brush, - IPath region) - => source.Clear(source.GetDrawingOptions(), brush, region); - - /// - /// Flood fills the image within the provided region defined by an using the specified brush - /// without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPath region) - => source.ApplyProcessor(new ClearPathProcessor(options, brush, region)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs deleted file mode 100644 index 0654942ac..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of rectangle outlines without blending. -/// -public static class ClearRectangleExtensions -{ - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified color without any blending. - /// - /// The source image processing context. - /// The color. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Color color, RectangleF rectangle) - => source.Clear(new SolidBrush(color), rectangle); - - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified color without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - RectangleF rectangle) - => source.Clear(options, new SolidBrush(color), rectangle); - - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified brush without any blending. - /// - /// The source image processing context. - /// The brush. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Brush brush, - RectangleF rectangle) - => source.Clear(brush, new RectangularPolygon(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)); - - /// - /// Flood fills the image at the given rectangle bounds with the specified brush without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - RectangleF rectangle) - => source.Clear(options, brush, new RectangularPolygon(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs deleted file mode 100644 index 10ac9ba3a..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the application of processors within a clipped path. -/// -public static class ClipPathExtensions -{ - /// - /// Applies the processing operation within the region defined by an . - /// - /// The source image processing context. - /// - /// The defining the clip region. Only pixels inside the clip are affected. - /// - /// - /// The operation to perform. This executes in the clipped context so results are constrained to the - /// clip bounds. - /// - /// The to allow chaining of operations. - public static IImageProcessingContext Clip( - this IImageProcessingContext source, - IPath region, - Action operation) - => source.ApplyProcessor(new ClipPathProcessor(source.GetDrawingOptions(), region, operation)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs deleted file mode 100644 index 707e53ac1..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of Bezier paths. -/// -public static class DrawBezierExtensions -{ - /// - /// Draws the provided points as an open Bezier path with the supplied pen - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path with the supplied pen - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(pen, new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(options, new SolidPen(brush, thickness), new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(new SolidPen(brush, thickness), new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawBeziers(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawBeziers(options, new SolidBrush(color), thickness, points); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs deleted file mode 100644 index d4fb0bef3..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of lines. -/// -public static class DrawLineExtensions -{ - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(options, new SolidPen(brush, thickness), new Path(points)); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The brush. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(new SolidPen(brush, thickness), new Path(points)); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The color. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawLine(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The line thickness. - /// The points. - /// The to allow chaining of operations.> - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawLine(options, new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open linear path with the supplied pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Path(points)); - - /// - /// Draws the provided points as an open linear path with the supplied pen. - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(pen, new Path(points)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs deleted file mode 100644 index 6726e2bfc..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of collections of polygon outlines. -/// -public static class DrawPathCollectionExtensions -{ - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - IPathCollection paths) - { - foreach (IPath path in paths) - { - source.Draw(options, pen, path); - } - - return source; - } - - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext - Draw(this IImageProcessingContext source, Pen pen, IPathCollection paths) - => source.Draw(source.GetDrawingOptions(), pen, paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shapes. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - IPathCollection paths) => - source.Draw(options, new SolidPen(brush, thickness), paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - IPathCollection paths) => - source.Draw(new SolidPen(brush, thickness), paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - IPathCollection paths) => - source.Draw(options, new SolidBrush(color), thickness, paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - IPathCollection paths) => - source.Draw(new SolidBrush(color), thickness, paths); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs deleted file mode 100644 index fd0ed2aa3..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of polygon outlines. -/// -public static class DrawPathExtensions -{ - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - IPath path) => - source.ApplyProcessor(new DrawPathProcessor(options, pen, path)); - - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw(this IImageProcessingContext source, Pen pen, IPath path) => - source.Draw(source.GetDrawingOptions(), pen, path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - IPath path) => - source.Draw(options, new SolidPen(brush, thickness), path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - IPath path) => - source.Draw(new SolidPen(brush, thickness), path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - IPath path) => - source.Draw(options, new SolidBrush(color), thickness, path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - IPath path) => - source.Draw(new SolidBrush(color), thickness, path); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs deleted file mode 100644 index 6ebad1e06..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of closed linear polygons. -/// -public static class DrawPolygonExtensions -{ - /// - /// Draws the provided points as a closed linear polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(source.GetDrawingOptions(), pen, new Polygon(points)); - - /// - /// Draws the provided points as a closed linear polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Polygon(points)); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.DrawPolygon(options, new SolidPen(brush, thickness), points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.DrawPolygon(new SolidPen(brush, thickness), points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawPolygon(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawPolygon(options, new SolidBrush(color), thickness, points); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs deleted file mode 100644 index 0971db357..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of rectangles. -/// -public static class DrawRectangleExtensions -{ - /// - /// Draws the outline of the rectangle with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - RectangleF shape) => - source.Draw(options, pen, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Draws the outline of the rectangle with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw(this IImageProcessingContext source, Pen pen, RectangleF shape) => - source.Draw(source.GetDrawingOptions(), pen, shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - RectangleF shape) => - source.Draw(options, new SolidPen(brush, thickness), shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - RectangleF shape) => - source.Draw(new SolidPen(brush, thickness), shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - RectangleF shape) => - source.Draw(options, new SolidBrush(color), thickness, shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - RectangleF shape) => - source.Draw(new SolidBrush(color), thickness, shape); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs deleted file mode 100644 index 9ad68315a..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of text. -/// -public static class DrawTextExtensions -{ - /// - /// Draws the text onto the image filled with the given color. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The color. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Color color, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, color, location); - - /// - /// Draws the text using the supplied drawing options onto the image filled with the given color. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The color. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Color color, - PointF location) => - source.DrawText(drawingOptions, text, font, Brushes.Solid(color), null, location); - - /// - /// Draws the text using the supplied text options onto the image filled via the brush. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Color color) => - source.DrawText(textOptions, text, Brushes.Solid(color), null); - - /// - /// Draws the text onto the image filled via the brush. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Brush brush, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, brush, location); - - /// - /// Draws the text onto the image outlined via the pen. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Pen pen, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, pen, location); - - /// - /// Draws the text onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Brush brush, - Pen pen, - PointF location) - { - RichTextOptions textOptions = new(font) { Origin = location }; - return source.DrawText(textOptions, text, brush, pen); - } - - /// - /// Draws the text using the given options onto the image filled via the brush. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Brush brush) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, brush, null); - - /// - /// Draws the text using the given options onto the image outlined via the pen. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Pen pen) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, null, pen); - - /// - /// Draws the text using the given options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Brush? brush, - Pen? pen) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, brush, pen); - - /// - /// Draws the text onto the image outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Pen pen, - PointF location) - => source.DrawText(drawingOptions, text, font, null, pen, location); - - /// - /// Draws the text onto the image filled via the brush. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Brush brush, - PointF location) - => source.DrawText(drawingOptions, text, font, brush, null, location); - - /// - /// Draws the text using the given drawing options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Brush? brush, - Pen? pen, - PointF location) - { - RichTextOptions textOptions = new(font) { Origin = location }; - return source.ApplyProcessor(new DrawTextProcessor(drawingOptions, textOptions, text, brush, pen)); - } - - /// - /// Draws the text using the given options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - RichTextOptions textOptions, - string text, - Brush? brush, - Pen? pen) - => source.ApplyProcessor(new DrawTextProcessor(drawingOptions, textOptions, text, brush, pen)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs deleted file mode 100644 index 86bb20c23..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of images. -/// -public static class FillExtensions -{ - /// - /// Flood fills the image with the specified color. - /// - /// The source image processing context. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, Color color) - => source.Fill(new SolidBrush(color)); - - /// - /// Flood fills the image with the specified color. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, DrawingOptions options, Color color) - => source.Fill(options, new SolidBrush(color)); - - /// - /// Flood fills the image with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, Brush brush) - => source.Fill(source.GetDrawingOptions(), brush); - - /// - /// Flood fills the image with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, DrawingOptions options, Brush brush) - => source.ApplyProcessor(new FillProcessor(options, brush)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs deleted file mode 100644 index 1629bdfa8..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of polygon outlines. -/// -public static class FillPathBuilderExtensions -{ - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified color. - /// - /// The source image processing context. - /// The color. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - Action region) - => source.Fill(new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified color. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - Action region) - => source.Fill(options, new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - Action region) - => source.Fill(source.GetDrawingOptions(), brush, region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified brush. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Action region) - { - PathBuilder pb = new(); - region(pb); - - return source.Fill(options, brush, pb.Build()); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs deleted file mode 100644 index 3b8cb4d8b..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Text; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of collections of polygon outlines. -/// -public static class FillPathCollectionExtensions -{ - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPathCollection paths) - { - foreach (IPath s in paths) - { - source.Fill(options, brush, s); - } - - return source; - } - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Pen pen, - IReadOnlyList paths) - => source.Fill(options, brush, pen, paths, static (gp, layer, path) => - { - if (layer.Kind == GlyphLayerKind.Decoration) - { - // Decorations (underlines, strikethroughs, etc) are always filled. - return true; - } - - if (layer.Kind == GlyphLayerKind.Glyph) - { - // Standard glyph layers are filled by default. - return true; - } - - // Default heuristic: stroke "background-like" layers (large coverage), fill others. - // Use the bounding box area as an approximation of the glyph area as it is cheaper to compute. - float glyphArea = gp.Bounds.Width * gp.Bounds.Height; - float layerArea = path.ComputeArea(); - - if (layerArea <= 0 || glyphArea <= 0) - { - return false; // degenerate glyph, don't fill - } - - float coverage = layerArea / glyphArea; - - // <50% coverage, fill. Otherwise, stroke. - return coverage < 0.50F; - }); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// - /// A function that decides whether to fill or stroke a given layer within a multi-layer (painted) glyph. - /// - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Pen pen, - IReadOnlyList paths, - Func shouldFillLayer) - { - foreach (GlyphPathCollection gp in paths) - { - if (gp.LayerCount == 0) - { - continue; - } - - if (gp.LayerCount == 1) - { - // Single-layer glyph: just fill with the supplied brush. - source.Fill(options, brush, gp.Paths); - continue; - } - - // Multi-layer: decide per layer whether to fill or stroke. - for (int i = 0; i < gp.Layers.Count; i++) - { - GlyphLayerInfo layer = gp.Layers[i]; - IPath path = gp.PathList[i]; - - if (shouldFillLayer(gp, layer, path)) - { - // Respect the layer's fill rule if different to the drawing options. - DrawingOptions o = options.CloneOrReturnForRules( - layer.IntersectionRule, - layer.PixelAlphaCompositionMode, - layer.PixelColorBlendingMode); - - source.Fill(o, brush, path); - } - else - { - // Outline only to preserve interior detail. - source.Draw(options, pen, path); - } - } - } - - return source; - } - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - IPathCollection paths) => - source.Fill(source.GetDrawingOptions(), brush, paths); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - Pen pen, - IReadOnlyList paths) => - source.Fill(source.GetDrawingOptions(), brush, pen, paths); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified color. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPathCollection paths) => - source.Fill(options, new SolidBrush(color), paths); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified color. - /// - /// The source image processing context. - /// The color. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IPathCollection paths) => - source.Fill(new SolidBrush(color), paths); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified color. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The color. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IReadOnlyList paths) => - source.Fill(new SolidBrush(color), new SolidPen(color), paths); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs deleted file mode 100644 index ef26eb107..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of polygon outlines. -/// -public static class FillPathExtensions -{ - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The color. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IPath path) => - source.Fill(new SolidBrush(color), path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPath path) => - source.Fill(options, new SolidBrush(color), path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - IPath path) => - source.Fill(source.GetDrawingOptions(), brush, path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPath path) => - source.ApplyProcessor(new FillPathProcessor(options, brush, path)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs deleted file mode 100644 index 6c87e509e..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of closed linear polygons. -/// -public static class FillPolygonExtensions -{ - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - params PointF[] points) => - source.Fill(options, brush, new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The brush. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - Brush brush, - params PointF[] points) => - source.Fill(brush, new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The options. - /// The color. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - params PointF[] points) => - source.Fill(options, new SolidBrush(color), new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The color. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - Color color, - params PointF[] points) => - source.Fill(new SolidBrush(color), new Polygon(points)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs deleted file mode 100644 index f8d8c7632..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of rectangles. -/// -public static class FillRectangleExtensions -{ - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - RectangleF shape) => - source.Fill(options, brush, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext - Fill(this IImageProcessingContext source, Brush brush, RectangleF shape) => - source.Fill(brush, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - RectangleF shape) => - source.Fill(options, new SolidBrush(color), shape); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The color. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext - Fill(this IImageProcessingContext source, Color color, RectangleF shape) => - source.Fill(new SolidBrush(color), shape); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs similarity index 96% rename from src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs rename to src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs index ddfc43baf..c27880c9e 100644 --- a/src/ImageSharp.Drawing/Processing/Extensions/ProcessWithCanvasExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - namespace SixLabors.ImageSharp.Drawing.Processing; /// diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs similarity index 94% rename from src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs rename to src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs index 8e41345a6..25b105f6c 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Processing.Processors; -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; +namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Defines a processor that executes a canvas callback for each image frame. diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs similarity index 95% rename from src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs rename to src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs index 938bd6723..130f8796b 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ProcessWithCanvasProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Processing.Processors; -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; +namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Executes a per-frame canvas callback for a specific pixel type. diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs deleted file mode 100644 index 900e4b29f..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to clear pixels within a given -/// with the given using clear composition semantics defined by . -/// -public class ClearPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The drawing options. - /// The details how to clear the region of interest. - /// The logic path to be cleared. - public ClearPathProcessor(DrawingOptions options, Brush brush, IPath path) - { - this.Region = path; - this.Brush = brush; - this.Options = options; - } - - /// - /// Gets the used for clearing the destination image. - /// - public Brush Brush { get; } - - /// - /// Gets the logic path that this processor applies to. - /// - public IPath Region { get; } - - /// - /// Gets the defining clear composition behavior. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new ClearPathProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs deleted file mode 100644 index c942b2fe7..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClearPathProcessor{TPixel}.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Uses a brush and a shape to clear the shape with clear composition semantics. -/// -/// The type of the color. -/// -internal class ClearPathProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly ClearPathProcessor definition; - private readonly IPath path; - private readonly Rectangle bounds; - - /// - /// Initializes a new instance of the class. - /// - /// The processing configuration. - /// The processor definition. - /// The source image. - /// The source bounds. - public ClearPathProcessor( - Configuration configuration, - ClearPathProcessor definition, - Image source, - Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - IPath path = definition.Region; - int left = (int)MathF.Floor(path.Bounds.Left); - int top = (int)MathF.Floor(path.Bounds.Top); - int right = (int)MathF.Ceiling(path.Bounds.Right); - int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); - - this.bounds = Rectangle.FromLTRB(left, top, right, bottom); - this.path = path; - this.definition = definition; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - Configuration configuration = this.Configuration; - Brush brush = this.definition.Brush; - - Rectangle interest = Rectangle.Intersect(this.bounds, source.Bounds); - if (interest.Equals(Rectangle.Empty)) - { - return; - } - - using DrawingCanvas canvas = new( - configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds), - this.definition.Options); - - canvas.Clear(this.path, brush); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs deleted file mode 100644 index a6a9bb475..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Allows the recursive application of processing operations against an image within a given region. -/// -public class ClipPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The drawing options. - /// The defining the region to operate within. - /// The operation to perform on the source. - public ClipPathProcessor(DrawingOptions options, IPath path, Action operation) - { - this.Options = options; - this.Region = path; - this.Operation = operation; - } - - /// - /// Gets the drawing options. - /// - public DrawingOptions Options { get; } - - /// - /// Gets the defining the region to operate within. - /// - public IPath Region { get; } - - /// - /// Gets the operation to perform on the source. - /// - public Action Operation { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor( - Configuration configuration, - Image source, - Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new ClipPathProcessor(this, source, configuration, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs deleted file mode 100644 index 5e4089c68..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Applies a processing operation to a clipped path region by constraining the operation's input domain -/// to the bounds of the path, then using the processed result as an image brush to fill the path. -/// -/// The type of pixel. -internal class ClipPathProcessor : IImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly ClipPathProcessor definition; - private readonly Image source; - private readonly Configuration configuration; - private readonly Rectangle sourceRectangle; - - public ClipPathProcessor(ClipPathProcessor definition, Image source, Configuration configuration, Rectangle sourceRectangle) - { - this.definition = definition; - this.source = source; - this.configuration = configuration; - this.sourceRectangle = sourceRectangle; - } - - public void Dispose() - { - } - - public void Execute() - { - // Bounds in drawing are floating point. We must conservatively cover the entire shape bounds. - RectangleF boundsF = this.definition.Region.Bounds; - - int left = (int)MathF.Floor(boundsF.Left); - int top = (int)MathF.Floor(boundsF.Top); - int right = (int)MathF.Ceiling(boundsF.Right); - int bottom = (int)MathF.Ceiling(boundsF.Bottom); - - Rectangle crop = Rectangle.FromLTRB(left, top, right, bottom); - - // Constrain the operation to the intersection of the requested bounds and source region. - Rectangle clipped = Rectangle.Intersect(this.sourceRectangle, crop); - - if (clipped.Width <= 0 || clipped.Height <= 0) - { - return; - } - - Action operation = this.definition.Operation; - - // Run the operation on the clipped context so only pixels inside the clip are affected, - // matching the expected semantics of clipping in other graphics APIs. - using Image clone = this.source.Clone(ctx => operation(ctx.Crop(clipped))); - - // Use the clone as a brush source so only the clipped result contributes to the fill, - // keeping the effect confined to the clipped region. - Point brushOffset = new( - clipped.X - (int)MathF.Floor(boundsF.Left), - clipped.Y - (int)MathF.Floor(boundsF.Top)); - - ImageBrush brush = new(clone, clone.Bounds, brushOffset); - - // Fill the shape using the image brush. - FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region); - using IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle); - pixelProcessor.Execute(); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs deleted file mode 100644 index f4335778c..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill pixels withing a given -/// with the given and blending defined by the given . -/// -public class DrawPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The graphics options. - /// The details how to outline the region of interest. - /// The path to be filled. - public DrawPathProcessor(DrawingOptions options, Pen pen, IPath path) - { - this.Path = path; - this.Pen = pen; - this.Options = options; - } - - /// - /// Gets the used for filling the destination image. - /// - public Pen Pen { get; } - - /// - /// Gets the path that this processor applies to. - /// - public IPath Path { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new DrawPathProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs deleted file mode 100644 index ec22ed8f3..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor{TPixel}.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Uses a pen and path to draw an outlined path through . -/// -/// The pixel format. -internal class DrawPathProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly DrawPathProcessor definition; - - /// - /// Initializes a new instance of the class. - /// - /// The processing configuration. - /// The processor definition. - /// The source image. - /// The source bounds. - public DrawPathProcessor( - Configuration configuration, - DrawPathProcessor definition, - Image source, - Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - => this.definition = definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - using DrawingCanvas canvas = new( - this.Configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds), - this.definition.Options); - - canvas.Draw(this.definition.Pen, this.definition.Path); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs deleted file mode 100644 index 01065c76b..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill pixels withing a given -/// with the given and blending defined by the given . -/// -public class FillPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The graphics options. - /// The details how to fill the region of interest. - /// The logic path to be filled. - public FillPathProcessor(DrawingOptions options, Brush brush, IPath path) - : this(options, brush, path, RasterizerSamplingOrigin.PixelBoundary) - { - } - - internal FillPathProcessor( - DrawingOptions options, - Brush brush, - IPath path, - RasterizerSamplingOrigin samplingOrigin) - { - this.Region = path; - this.Brush = brush; - this.Options = options; - this.SamplingOrigin = samplingOrigin; - } - - /// - /// Gets the used for filling the destination image. - /// - public Brush Brush { get; } - - /// - /// Gets the logic path that this processor applies to. - /// - public IPath Region { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - internal RasterizerSamplingOrigin SamplingOrigin { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new FillPathProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs deleted file mode 100644 index d44a46fcb..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Uses a brush and a shape to fill the shape with contents of the brush. -/// -/// The type of the color. -/// -internal class FillPathProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly FillPathProcessor definition; - private readonly IPath path; - private readonly Rectangle bounds; - - /// - /// Initializes a new instance of the class. - /// - /// The processing configuration. - /// The processor definition. - /// The source image. - /// The source bounds. - public FillPathProcessor( - Configuration configuration, - FillPathProcessor definition, - Image source, - Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - IPath path = definition.Region; - int left = (int)MathF.Floor(path.Bounds.Left); - int top = (int)MathF.Floor(path.Bounds.Top); - int right = (int)MathF.Ceiling(path.Bounds.Right); - int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); - - this.bounds = Rectangle.FromLTRB(left, top, right, bottom); - this.path = path; - this.definition = definition; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - Configuration configuration = this.Configuration; - Brush brush = this.definition.Brush; - - // Align start/end positions. - Rectangle interest = Rectangle.Intersect(this.bounds, source.Bounds); - if (interest.Equals(Rectangle.Empty)) - { - return; // No effect inside image; - } - - using DrawingCanvas canvas = new( - configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds), - this.definition.Options); - - canvas.Fill(this.path, brush); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs deleted file mode 100644 index cec760e71..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill an with the given -/// using blending defined by the given . -/// -public class FillProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The defining how to blend the brush pixels over the image pixels. - /// The brush to use for filling. - public FillProcessor(DrawingOptions options, Brush brush) - { - this.Brush = brush; - this.Options = options; - } - - /// - /// Gets the used for filling the destination image. - /// - public Brush Brush { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new FillProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs deleted file mode 100644 index 5bfe77c8a..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Using the brush as a source of pixels colors blends the brush color with source. -/// -/// The pixel format. -internal class FillProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly FillProcessor definition; - - public FillProcessor(Configuration configuration, FillProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - => this.definition = definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); - if (interest.Width == 0 || interest.Height == 0) - { - return; - } - - using DrawingCanvas canvas = new( - this.Configuration, - new Buffer2DRegion(source.PixelBuffer, interest), - this.definition.Options); - - canvas.Fill(this.definition.Brush); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs deleted file mode 100644 index 0eba43ace..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -/// -/// Defines a processor to draw text on an . -/// -public class DrawTextProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The drawing options. - /// The text rendering options. - /// The text we want to render - /// The brush to source pixel colors from. - /// The pen to outline text with. - public DrawTextProcessor(DrawingOptions drawingOptions, RichTextOptions textOptions, string text, Brush? brush, Pen? pen) - { - Guard.NotNull(text, nameof(text)); - if (brush is null && pen is null) - { - throw new ArgumentException($"Expected a {nameof(brush)} or {nameof(pen)}. Both were null"); - } - - this.DrawingOptions = drawingOptions; - this.TextOptions = textOptions; - this.Text = text; - this.Brush = brush; - this.Pen = pen; - } - - /// - /// Gets the brush used to fill the glyphs. - /// - public Brush? Brush { get; } - - /// - /// Gets the defining blending modes and shape drawing settings. - /// - public DrawingOptions DrawingOptions { get; } - - /// - /// Gets the defining text-specific drawing settings. - /// - public RichTextOptions TextOptions { get; } - - /// - /// Gets the text to draw. - /// - public string Text { get; } - - /// - /// Gets the pen used for outlining the text, if Null then we will not outline - /// - public Pen? Pen { get; } - - /// - /// Gets the location to draw the text at. - /// - public PointF Location { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new DrawTextProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs deleted file mode 100644 index 23dce9646..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -/// -/// Using the brush as a source of pixels colors blends the brush color with source. -/// -/// The pixel format. -internal class DrawTextProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly DrawTextProcessor definition; - - public DrawTextProcessor(Configuration configuration, DrawTextProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - => this.definition = definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - using DrawingCanvas canvas = new( - this.Configuration, - new Buffer2DRegion(source.PixelBuffer, source.Bounds), - this.definition.DrawingOptions); - - canvas.DrawText( - this.definition.TextOptions, - this.definition.Text, - this.definition.Brush, - this.definition.Pen); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs similarity index 100% rename from src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs rename to src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs similarity index 100% rename from src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs rename to src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs index 6ff494267..7faef9715 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs @@ -4,7 +4,6 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using System.Numerics; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; @@ -19,43 +18,34 @@ public class DrawBeziers [Benchmark(Baseline = true, Description = "System.Drawing Draw Beziers")] public void DrawPathSystemDrawing() { - using (Bitmap destination = new(800, 800)) - using (Graphics graphics = Graphics.FromImage(destination)) - { - graphics.InterpolationMode = InterpolationMode.Default; - graphics.SmoothingMode = SmoothingMode.AntiAlias; - - using (Pen pen = new(System.Drawing.Color.HotPink, 10)) - { - graphics.DrawBeziers( - pen, - [new SDPointF(10, 500), new SDPointF(30, 10), new SDPointF(240, 30), new SDPointF(300, 500)]); - } + using Bitmap destination = new(800, 800); + using Graphics graphics = Graphics.FromImage(destination); + graphics.InterpolationMode = InterpolationMode.Default; + graphics.SmoothingMode = SmoothingMode.AntiAlias; - using (MemoryStream stream = new()) - { - destination.Save(stream, ImageFormat.Bmp); - } + using (Pen pen = new(System.Drawing.Color.HotPink, 10)) + { + graphics.DrawBeziers( + pen, + [new SDPointF(10, 500), new SDPointF(30, 10), new SDPointF(240, 30), new SDPointF(300, 500)]); } + + using MemoryStream stream = new(); + destination.Save(stream, ImageFormat.Bmp); } [Benchmark(Description = "ImageSharp Draw Beziers")] public void DrawLinesCore() { - using (Image image = new(800, 800)) - { - image.Mutate(x => x.DrawBeziers( - Color.HotPink, - 10, - new Vector2(10, 500), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 500))); + using Image image = new(800, 800); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawBezier( + Processing.Pens.Solid(Color.HotPink, 10), + new PointF(10, 500), + new PointF(30, 10), + new PointF(240, 30), + new PointF(300, 500)))); - using (MemoryStream stream = new()) - { - image.SaveAsBmp(stream); - } - } + using MemoryStream stream = new(); + image.SaveAsBmp(stream); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 82e89c16f..b47dbf640 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -167,7 +167,11 @@ public void SystemDrawing() // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. [Benchmark] public void ImageSharpCombinedPathsScanlineRasterizer() - => this.image.Mutate(c => c.SetRasterizer(ScanlineRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); + => this.image.Mutate(c => + { + c.SetRasterizer(ScanlineRasterizer.Instance); + c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath)); + }); [Benchmark] public void ImageSharpSeparatePathsScanlineRasterizer() @@ -176,31 +180,34 @@ public void ImageSharpSeparatePathsScanlineRasterizer() { // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. c.SetRasterizer(ScanlineRasterizer.Instance); - foreach (PointF[] loop in this.points) + c.ProcessWithCanvas(canvas => { - c.DrawPolygon(Color.White, this.Thickness, loop); - } + foreach (PointF[] loop in this.points) + { + canvas.Draw(Processing.Pens.Solid(Color.White, this.Thickness), new Polygon(loop)); + } + }); }); // Tiled is now the framework default rasterizer path. [Benchmark] public void ImageSharpCombinedPathsTiled() - => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] public void ImageSharpCombinedPathsWebGPUBackend() - => this.webGpuImage.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark] public void ImageSharpSeparatePathsTiled() => this.image.Mutate( - c => - { - foreach (PointF[] loop in this.points) + c => c.ProcessWithCanvas(canvas => { - c.DrawPolygon(Color.White, this.Thickness, loop); - } - }); + foreach (PointF[] loop in this.points) + { + canvas.Draw(Processing.Pens.Solid(Color.White, this.Thickness), new Polygon(loop)); + } + })); [Benchmark(Baseline = true)] public void SkiaSharp() @@ -210,10 +217,12 @@ public void SkiaSharp() public IPath ImageSharpStrokeAndClip() => this.isPen.GeneratePath(this.imageSharpPath); [Benchmark] - public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); + public void FillPolygon() + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(this.strokedImageSharpPath, Processing.Brushes.Solid(Color.White)))); [Benchmark] - public void FillPolygonWebGPUBackend() => this.webGpuImage.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); + public void FillPolygonWebGPUBackend() + => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(this.strokedImageSharpPath, Processing.Brushes.Solid(Color.White)))); } public class DrawPolygonAll : DrawPolygon @@ -239,7 +248,7 @@ protected override PointF[][] GetPoints(FeatureCollection features) Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs index 21dedf417..201eeed0f 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs @@ -84,7 +84,8 @@ public void ImageSharp() Origin = new PointF(10, 10) }; - this.image.Mutate(x => x.DrawText(textOptions, this.TextToRender, Processing.Brushes.Solid(Color.HotPink))); + this.image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.DrawText(textOptions, this.TextToRender, Processing.Brushes.Solid(Color.HotPink), pen: null))); } [Benchmark(Baseline = true)] diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs index 245d7e852..48e9e95fd 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs @@ -60,10 +60,11 @@ public void DrawTextCore() Origin = new PointF(10, 10) }; - image.Mutate(x => x.DrawText( + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText( textOptions, this.TextToRender, - Processing.Pens.Solid(Color.HotPink, 10))); + brush: null, + pen: Processing.Pens.Solid(Color.HotPink, 10)))); } [Benchmark(Description = "ImageSharp Draw Text Outline - Naive")] @@ -79,17 +80,17 @@ public void DrawTextCoreOld() }; image.Mutate( - x => DrawTextOldVersion( - x, + x => x.ProcessWithCanvas(canvas => DrawTextOldVersion( + canvas, new DrawingOptions { GraphicsOptions = { Antialias = true } }, textOptions, this.TextToRender, null, - Processing.Pens.Solid(Color.HotPink, 10))); + Processing.Pens.Solid(Color.HotPink, 10)))); } - static IImageProcessingContext DrawTextOldVersion( - IImageProcessingContext source, + static void DrawTextOldVersion( + IDrawingCanvas canvas, DrawingOptions options, TextOptions textOptions, string text, @@ -97,19 +98,23 @@ static IImageProcessingContext DrawTextOldVersion( Pen pen) { IPathCollection glyphs = TextBuilder.GeneratePaths(text, textOptions); - - DrawingOptions pathOptions = new() { GraphicsOptions = options.GraphicsOptions }; - if (brush != null) + int saveCount = canvas.Save(options); + try { - source.Fill(pathOptions, brush, glyphs); + if (brush != null) + { + canvas.Fill(brush, glyphs); + } + + if (pen != null) + { + canvas.Draw(pen, glyphs); + } } - - if (pen != null) + finally { - source.Draw(pathOptions, pen, glyphs); + canvas.RestoreTo(saveCount); } - - return source; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 219c9eb96..122c7cd50 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -26,19 +26,7 @@ public class DrawTextRepeatedGlyphs } }; - private readonly DrawingOptions clearOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - } - }; - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly Brush clearBrush = Brushes.Solid(Color.Transparent); private Configuration defaultConfiguration; private Image defaultImage; @@ -117,7 +105,7 @@ public void Cleanup() public void DrawingCanvasDefaultBackend() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.defaultImage)); - // this.ClearWithDrawingCanvas(this.defaultConfiguration, frame); + using DrawingCanvas canvas = new(this.defaultConfiguration, frame, this.drawingOptions); canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); @@ -127,7 +115,7 @@ public void DrawingCanvasDefaultBackend() public void DrawingCanvasWebGPUBackendCpuRegion() { CpuRegionOnlyFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); - // this.ClearWithDrawingCanvas(this.webGpuConfiguration, frame); + using DrawingCanvas canvas = new(this.webGpuConfiguration, frame, this.drawingOptions); canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); @@ -136,19 +124,11 @@ public void DrawingCanvasWebGPUBackendCpuRegion() [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] public void DrawingCanvasWebGPUBackendNativeSurface() { - // this.ClearWithDrawingCanvas(this.webGpuConfiguration, this.webGpuNativeFrame); using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, this.drawingOptions); canvas.DrawText(this.textOptions, this.text, this.brush, null); canvas.Flush(); } - private void ClearWithDrawingCanvas(Configuration configuration, ICanvasFrame target) - { - using DrawingCanvas canvas = new(configuration, target, this.clearOptions); - canvas.Fill(this.clearBrush); - canvas.Flush(); - } - private static Buffer2DRegion GetFrameRegion(Image image) => new(image.Frames.RootFrame.PixelBuffer, new Rectangle(0, 0, image.Width, image.Height)); diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs index f5c356061..9c1272246 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs @@ -34,9 +34,11 @@ public void DrawImageSharp() float y = this.Rand(this.height); EllipsePolygon ellipse = new(new PointF(x, y), r); this.image.Mutate( - m => - m.Fill(Brushes.Solid(brushColor), ellipse) - .Draw(Pens.Solid(penColor, this.Rand(5)), ellipse)); + m => m.ProcessWithCanvas(canvas => + { + canvas.Fill(ellipse, Brushes.Solid(brushColor)); + canvas.Draw(Pens.Solid(penColor, this.Rand(5)), ellipse); + })); } } @@ -49,5 +51,5 @@ public void Cleanup() [MethodImpl(MethodImplOptions.AggressiveInlining)] private float Rand(float x) - => ((float)(((this.random.Next() << 15) | this.random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000f; + => Math.Max(0.5f, ((float)(((this.random.Next() << 15) | this.random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000f); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs index acac46b09..c83b5880d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs @@ -31,6 +31,6 @@ public void FillGradientBrush_ImageSharp() PathGradientBrush brush = new(points, colors, Color.White); - this.image.Mutate(x => x.Fill(brush)); + this.image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.Fill(brush))); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs index 60ba9b199..c7e79c67c 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs @@ -36,9 +36,7 @@ public abstract class FillPolygon protected abstract int Height { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) - => features.Features - .SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))) - .ToArray(); + => [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60)))]; [GlobalSetup] public void Setup() @@ -48,9 +46,9 @@ public void Setup() FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); this.points = this.GetPoints(featureCollection); - this.polygons = this.points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray(); + this.polygons = [.. this.points.Select(pts => new Polygon(new LinearLineSegment(pts)))]; - this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); + this.sdPoints = [.. this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray())]; this.skPaths = []; foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) @@ -102,13 +100,13 @@ public void SystemDrawing() [Benchmark] public void ImageSharp() - => this.image.Mutate(c => + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => { foreach (Polygon polygon in this.polygons) { - c.Fill(Color.White, polygon); + canvas.Fill(polygon, Processing.Brushes.Solid(Color.White)); } - }); + })); [Benchmark(Baseline = true)] public void SkiaSharp() @@ -146,7 +144,7 @@ protected override PointF[][] GetPoints(FeatureCollection features) Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } // ** 11/13/2020 @ Anton's PC *** @@ -176,6 +174,6 @@ protected override PointF[][] GetPoints(FeatureCollection features) Matrix3x2 transform = Matrix3x2.CreateTranslation(-60, -40) * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs index b5d04ec28..3af511560 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs @@ -3,7 +3,6 @@ using System.Drawing; using System.Drawing.Drawing2D; -using System.Numerics; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; @@ -34,7 +33,8 @@ public Size FillRectangleCore() { using (Image image = new(800, 800)) { - image.Mutate(x => x.Fill(Color.HotPink, new Rectangle(10, 10, 190, 140))); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill(new Rectangle(10, 10, 190, 140), Processing.Brushes.Solid(Color.HotPink)))); return new Size(image.Width, image.Height); } @@ -45,12 +45,16 @@ public Size FillPolygonCore() { using (Image image = new(800, 800)) { - image.Mutate(x => x.FillPolygon( - Color.HotPink, - new Vector2(10, 10), - new Vector2(200, 10), - new Vector2(200, 150), - new Vector2(10, 150))); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill( + new Polygon( + [ + new PointF(10, 10), + new PointF(200, 10), + new PointF(200, 150), + new PointF(10, 150) + ]), + Processing.Brushes.Solid(Color.HotPink)))); return new Size(image.Width, image.Height); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs index 63fc8bcc6..b0b250c4b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs @@ -17,34 +17,27 @@ public class FillWithPattern [Benchmark(Baseline = true, Description = "System.Drawing Fill with Pattern")] public void DrawPatternPolygonSystemDrawing() { - using (Bitmap destination = new(800, 800)) - using (Graphics graphics = Graphics.FromImage(destination)) - { - graphics.SmoothingMode = SmoothingMode.AntiAlias; - - using (HatchBrush brush = new(HatchStyle.BackwardDiagonal, System.Drawing.Color.HotPink)) - { - graphics.FillRectangle(brush, new SDRectangle(0, 0, 800, 800)); // can't find a way to flood fill with a brush - } + using Bitmap destination = new(800, 800); + using Graphics graphics = Graphics.FromImage(destination); + graphics.SmoothingMode = SmoothingMode.AntiAlias; - using (MemoryStream stream = new()) - { - destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp); - } + using (HatchBrush brush = new(HatchStyle.BackwardDiagonal, System.Drawing.Color.HotPink)) + { + graphics.FillRectangle(brush, new SDRectangle(0, 0, 800, 800)); // can't find a way to flood fill with a brush } + + using MemoryStream stream = new(); + destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp); } [Benchmark(Description = "ImageSharp Fill with Pattern")] public void DrawPatternPolygon3Core() { - using (Image image = new(800, 800)) - { - image.Mutate(x => x.Fill(CoreBrushes.BackwardDiagonal(Color.HotPink))); + using Image image = new(800, 800); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill(CoreBrushes.BackwardDiagonal(Color.HotPink)))); - using (MemoryStream stream = new()) - { - image.SaveAsBmp(stream); - } - } + using MemoryStream stream = new(); + image.SaveAsBmp(stream); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs deleted file mode 100644 index 45f09159f..000000000 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -using BenchmarkDotNet.Attributes; - -namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; -public class Rounding -{ - private PointF[] vertices; - private float[] destination; - private float[] destinationSse41; - private float[] destinationAvx; - - [GlobalSetup] - public void Setup() - { - this.vertices = new PointF[1000]; - this.destination = new float[this.vertices.Length]; - this.destinationSse41 = new float[this.vertices.Length]; - this.destinationAvx = new float[this.vertices.Length]; - Random r = new(42); - for (int i = 0; i < this.vertices.Length; i++) - { - this.vertices[i] = new PointF((float)r.NextDouble(), (float)r.NextDouble()); - } - } - - [Benchmark] - public void RoundYAvx() => RoundYAvx(this.vertices, this.destinationAvx, 16); - - [Benchmark] - public void RoundYSse41() => RoundYSse41(this.vertices, this.destinationSse41, 16); - - [Benchmark(Baseline = true)] - public void RoundY() => RoundY(this.vertices, this.destination, 16); - - private static void RoundYAvx(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - int ri = 0; - if (Avx.IsSupported) - { - // If the length of the input buffer as a float array is a multiple of 16, we can use AVX instructions: - int verticesLengthInFloats = vertices.Length * 2; - int vector256FloatCount_x2 = Vector256.Count * 2; - int remainder = verticesLengthInFloats % vector256FloatCount_x2; - int verticesLength = verticesLengthInFloats - remainder; - - if (verticesLength > 0) - { - ri = vertices.Length - (remainder / 2); - nint maxIterations = verticesLength / (Vector256.Count * 2); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - Vector256 ssRatio = Vector256.Create(subsamplingRatio); - Vector256 inverseSsRatio = Vector256.Create(1F / subsamplingRatio); - - // For every 1 vector we add to the destination we read 2 from the vertices. - for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) - { - // Load 8 PointF - Vector256 points1 = Unsafe.Add(ref sourceBase, j); - Vector256 points2 = Unsafe.Add(ref sourceBase, j + 1); - - // Shuffle the points to group the Y properties - Vector128 points1Y = Sse.Shuffle(points1.GetLower(), points1.GetUpper(), 0b11_01_11_01); - Vector128 points2Y = Sse.Shuffle(points2.GetLower(), points2.GetUpper(), 0b11_01_11_01); - Vector256 pointsY = Vector256.Create(points1Y, points2Y); - - // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. - // https://www.ocf.berkeley.edu/~horie/rounding.html - Vector256 rounded = Avx.RoundToPositiveInfinity(Avx.Multiply(pointsY, ssRatio)); - Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); - } - } - } - - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } - - private static void RoundYSse41(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - int ri = 0; - if (Sse41.IsSupported) - { - // If the length of the input buffer as a float array is a multiple of 8, we can use Sse instructions: - int verticesLengthInFloats = vertices.Length * 2; - int vector128FloatCount_x2 = Vector128.Count * 2; - int remainder = verticesLengthInFloats % vector128FloatCount_x2; - int verticesLength = verticesLengthInFloats - remainder; - - if (verticesLength > 0) - { - ri = vertices.Length - (remainder / 2); - nint maxIterations = verticesLength / (Vector128.Count * 2); - ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); - ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - Vector128 ssRatio = Vector128.Create(subsamplingRatio); - Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); - - // For every 1 vector we add to the destination we read 2 from the vertices. - for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) - { - // Load 4 PointF - Vector128 points1 = Unsafe.Add(ref sourceBase, j); - Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); - - // Shuffle the points to group the Y properties - Vector128 points1Y = Sse.Shuffle(points1, points1, 0b11_01_11_01); - Vector128 points2Y = Sse.Shuffle(points2, points2, 0b11_01_11_01); - Vector128 pointsY = Vector128.Create(points1Y.GetLower(), points2Y.GetLower()); - - // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. - // https://www.ocf.berkeley.edu/~horie/rounding.html - Vector128 rounded = Sse41.RoundToPositiveInfinity(Sse.Multiply(pointsY, ssRatio)); - Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); - } - } - } - - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } - - private static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - int ri = 0; - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs similarity index 91% rename from tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs rename to tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs index d7d7bb4a1..cba7efcec 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; +namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; public class ComputeLength { diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs deleted file mode 100644 index 00b538d66..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using GeoJSON.Net.Feature; -using Newtonsoft.Json; -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -public class DrawingProfilingBenchmarks : IDisposable -{ - private readonly Image image; - private readonly Polygon[] polygons; - - public DrawingProfilingBenchmarks() - { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); - - FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); - - PointF[][] points = GetPoints(featureCollection); - this.polygons = points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray(); - - this.image = new Image(1000, 1000); - - static PointF[][] GetPoints(FeatureCollection features) - { - Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); - } - } - - [Theory(Skip = "For local profiling only")] - [InlineData(IntersectionRule.EvenOdd)] - [InlineData(IntersectionRule.NonZero)] - public void FillPolygon(IntersectionRule intersectionRule) - { - const int times = 100; - - for (int i = 0; i < times; i++) - { - this.image.Mutate(c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - foreach (Polygon polygon in this.polygons) - { - c.Fill(Color.White, polygon); - } - }); - } - } - - [Theory(Skip = "For local profiling only")] - [InlineData(1)] - [InlineData(10)] - public void DrawText(int textIterations) - { - const int times = 20; - const string textPhrase = "asdfghjkl123456789{}[]+$%?"; - string textToRender = string.Join("/n", Enumerable.Repeat(textPhrase, textIterations)); - - Font font = SystemFonts.CreateFont("Arial", 12); - SolidBrush brush = Brushes.Solid(Color.HotPink); - RichTextOptions textOptions = new(font) - { - WrappingLength = 780, - Origin = new PointF(10, 10) - }; - - for (int i = 0; i < times; i++) - { - this.image.Mutate(x => x - .SetGraphicsOptions(o => o.Antialias = true) - .DrawText( - textOptions, - textToRender, - brush)); - } - } - - public void Dispose() => this.image.Dispose(); -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs deleted file mode 100644 index 178dfa48e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class Clear : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new() - { - GraphicsOptions = - { - AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Clear, - BlendPercentage = 0.5f, - ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken - } - }; - - private readonly Brush brush = new SolidBrush(Color.HotPink); - - [Fact] - public void Brush() - { - this.operations.Clear(this.nonDefaultOptions, this.brush); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions.ShapeOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(this.nonDefaultOptions, Color.Red); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorSetDefaultOptions() - { - this.operations.Clear(Color.Red); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs deleted file mode 100644 index 155602407..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class ClearPath : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new() - { - GraphicsOptions = - { - AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Clear, - BlendPercentage = 0.5f, - ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken - } - }; - - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = new Star(1, 10, 5, 23, 56); - - [Fact] - public void Brush() - { - this.operations.Clear(this.nonDefaultOptions, this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(this.nonDefaultOptions, Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Clear(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs deleted file mode 100644 index c7d249851..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class ClearRectangle : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void Brush() - { - this.operations.Clear(new DrawingOptions(), this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(new DrawingOptions(), Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Clear(Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs deleted file mode 100644 index 91d566714..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawBezier : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new(10, 10), - new(20, 20), - new(20, 50), - new(50, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - Path innerPath = Assert.IsType(path); - ILineSegment segment = Assert.Single(innerPath.LineSegments); - CubicBezierLineSegment bezierSegment = Assert.IsType(segment); - Assert.Equal(expectedPoints, bezierSegment.ControlPoints.ToArray()); - - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.False(simplePath.IsClosed); - } - - [Fact] - public void Pen() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawBeziers(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawBeziers(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawBeziers(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawBeziers(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawBeziers(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs deleted file mode 100644 index 5ab5ae86c..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawLine : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new(10, 10), - new(20, 20), - new(20, 50), - new(50, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.False(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Pen() - { - this.operations.DrawLine(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawLine(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawLine(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawLine(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawLine(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawLine(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawLine(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawLine(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs deleted file mode 100644 index 8c283ed26..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPath : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly IPath path = new EllipsePolygon(10, 10, 100); - - [Fact] - public void Pen() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.Draw(this.pen, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Red, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Red, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs deleted file mode 100644 index cb104bbbd..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPathCollection : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 1); - private readonly IPath path1 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPath path2 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPathCollection pathCollection; - - public DrawPathCollection() - => this.pathCollection = new PathCollection(this.path1, this.path2); - - [Fact] - public void Pen() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen, p.Pen); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.Draw(this.pen, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen, p.Pen); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen.StrokeFill, p.Pen.StrokeFill); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen.StrokeFill, p.Pen.StrokeFill); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Pink, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Pen.StrokeFill); - Assert.Equal(Color.Pink, brush.Color); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Pink, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Pen.StrokeFill); - Assert.Equal(Color.Pink, brush.Color); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs deleted file mode 100644 index fbc3cbee3..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPolygon : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new PointF(10, 10), - new PointF(10, 20), - new PointF(20, 20), - new PointF(25, 25), - new PointF(25, 10) - ]; - - private static void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Pen() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawPolygon(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawPolygon(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawPolygon(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawPolygon(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawPolygon(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs deleted file mode 100644 index 5e5ed3304..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawRectangle : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void CorrectlySetsPenAndPath() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void CorrectlySetsPenAndPathDefaultOptions() - { - this.operations.Draw(this.pen, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Red, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Red, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs deleted file mode 100644 index f4cdcd2bf..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class Fill : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new(); - private readonly Brush brush = new SolidBrush(Color.HotPink); - - [Fact] - public void Brush() - { - this.operations.Fill(this.nonDefaultOptions, this.brush); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions, processor.Options); - Assert.Equal(expectedOptions.GraphicsOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.GraphicsOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.GraphicsOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush); - - FillProcessor processor = this.Verify(); - - GraphicsOptions expectedOptions = this.graphicsOptions; - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(this.nonDefaultOptions, Color.Red); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions, processor.Options); - - Assert.Equal(expectedOptions.GraphicsOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.GraphicsOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.GraphicsOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorSetDefaultOptions() - { - this.operations.Fill(Color.Red); - - FillProcessor processor = this.Verify(); - - GraphicsOptions expectedOptions = this.graphicsOptions; - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs deleted file mode 100644 index bf895e21e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPath : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = new Star(1, 10, 5, 23, 56); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs deleted file mode 100644 index 070f2577e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPathBuilder : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = null; - private readonly Action builder = pb => - { - pb.StartFigure(); - pb.AddLine(10, 10, 20, 20); - pb.AddLine(60, 450, 120, 340); - pb.AddLine(120, 340, 10, 10); - pb.CloseAllFigures(); - }; - - public FillPathBuilder() - { - PathBuilder pb = new(); - this.builder(pb); - this.path = pb.Build(); - } - - private void VerifyPoints(IPath expectedPath, IPath path) - { - ISimplePath simplePathExpected = Assert.Single(expectedPath.Flatten()); - PointF[] expectedPoints = simplePathExpected.Points.ToArray(); - - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs deleted file mode 100644 index aca2d2e05..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPathCollection : BaseImageOperationsExtensionTest -{ - private readonly Color color = Color.HotPink; - private readonly SolidBrush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path1 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPath path2 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPathCollection pathCollection; - - public FillPathCollection() - => this.pathCollection = new PathCollection(this.path1, this.path2); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.brush, p.Brush); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void BrushWithDefault() - { - this.operations.Fill(this.brush, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.brush, p.Brush); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Pink, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Brush); - Assert.Equal(Color.Pink, brush.Color); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void ColorWithDefault() - { - this.operations.Fill(Color.Pink, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Brush); - Assert.Equal(Color.Pink, brush.Color); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs deleted file mode 100644 index bac4ffb04..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPolygon : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly PointF[] path = - [ - new PointF(10, 10), - new PointF(10, 20), - new PointF(20, 20), - new PointF(25, 25), - new PointF(25, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Brush() - { - this.operations.FillPolygon(new DrawingOptions(), this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.FillPolygon(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.FillPolygon(new DrawingOptions(), Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.FillPolygon(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs deleted file mode 100644 index a13537d46..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillRectangle : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs index e7d017c8b..08a859061 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; using SixLabors.ImageSharp.Drawing.Tests.Processing; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs deleted file mode 100644 index 4355855ee..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Text; - -public class DrawText : BaseImageOperationsExtensionTest -{ - private readonly FontCollection fontCollection; - private readonly RichTextOptions textOptions; - private readonly DrawingOptions otherDrawingOptions = new() - { - GraphicsOptions = new GraphicsOptions() - }; - - private readonly Font font; - - public DrawText() - { - this.fontCollection = new FontCollection(); - this.font = this.fontCollection.Add(TestFontUtilities.GetPath("SixLaborsSampleAB.woff")).CreateFont(12); - this.textOptions = new RichTextOptions(this.font) { WrappingLength = 99 }; - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetAndNotPen() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - Brushes.Solid(Color.Red), - null, - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetAndNotPenDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Brushes.Solid(Color.Red)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Brushes.Solid(Color.Red), Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Brushes.Solid(Color.Red)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenColorSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Color.Red, Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenColorSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Color.Red); - - DrawTextProcessor processor = this.Verify(0); - - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndNotBrush() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - null, - Pens.Dash(Color.Red, 1), - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndNotBrushDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Pens.Dash(Color.Red, 1)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Pens.Dash(Color.Red, 1), Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Pens.Dash(Color.Red, 1)); - - DrawTextProcessor processor = this.Verify(0); - - Assert.Equal("123", processor.Text); - Assert.Equal(this.font, processor.TextOptions.Font); - SolidBrush penBrush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, penBrush.Color); - PatternPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(1, processorPen.StrokeWidth); - Assert.Equal(PointF.Empty, processor.Location); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndFillFroEachWhenBrushSet() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - Brushes.Solid(Color.Red), - Pens.Dash(Color.Red, 1), - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - - Assert.Equal("123", processor.Text); - Assert.Equal(this.font, processor.TextOptions.Font); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(PointF.Empty, processor.Location); - SolidBrush penBrush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, penBrush.Color); - PatternPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(1, processorPen.StrokeWidth); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs deleted file mode 100644 index f2d71700b..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Reflection; -using Moq; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Shapes; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Tests.Processing; - -public class FillPathProcessorTests -{ - [Fact] - public void FillOffCanvas() - { - Rectangle bounds = new(-100, -10, 10, 10); - - // Specifically not using RectangularPolygon here to ensure the FillPathProcessor is used. - LinearLineSegment[] points = - [ - new(new PointF(bounds.Left, bounds.Top), new PointF(bounds.Right, bounds.Top)), - new(new PointF(bounds.Right, bounds.Top), new PointF(bounds.Right, bounds.Bottom)), - new(new PointF(bounds.Right, bounds.Bottom), new PointF(bounds.Left, bounds.Bottom)), - new(new PointF(bounds.Left, bounds.Bottom), new PointF(bounds.Left, bounds.Top)) - ]; - Path path = new(points); - Mock brush = new(); - GraphicsOptions options = new() { Antialias = true }; - FillPathProcessor processor = new(new DrawingOptions() { GraphicsOptions = options }, brush.Object, path); - Image img = new(10, 10); - processor.Execute(img.Configuration, img, bounds); - } - - [Fact] - public void DrawOffCanvas() - { - using (Image img = new(10, 10)) - { - img.Mutate(x => x.DrawLine( - new SolidPen(Color.Black, 10), - new Vector2(-10, 5), - new Vector2(20, 5))); - } - } - - [Fact] - public void OtherShape() - { - Rectangle imageSize = new(0, 0, 500, 500); - EllipsePolygon path = new(1, 1, 23); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - Assert.IsType>(pixelProcessor); - } - - [Fact] - public void RectangleFloatAndAntialias() - { - Rectangle imageSize = new(0, 0, 500, 500); - RectangleF floatRect = new(10.5f, 10.5f, 400.6f, 400.9f); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(floatRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - Assert.IsType>(pixelProcessor); - } - - [Fact] - public void IntRectangle() - { - Rectangle imageSize = new(0, 0, 500, 500); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(expectedRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - FillProcessor fill = Assert.IsType>(pixelProcessor); - Assert.Equal(expectedRect, fill.GetProtectedValue("SourceRectangle")); - } - - [Fact] - public void FloatRectAntialiasingOff() - { - Rectangle imageSize = new(0, 0, 500, 500); - RectangleF floatRect = new(10.5f, 10.5f, 400.6f, 400.9f); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(floatRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = false } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - FillProcessor fill = Assert.IsType>(pixelProcessor); - - Assert.Equal(expectedRect, fill.GetProtectedValue("SourceRectangle")); - } - - [Fact] - public void DoesNotThrowForIssue928() - { - RectangleF rectText = new(0, 0, 2000, 2000); - using (Image img = new((int)rectText.Width, (int)rectText.Height)) - { - img.Mutate(x => x.Fill(Color.Transparent)); - - img.Mutate( - ctx => ctx.DrawLine( - Color.Red, - 0.984252f, - new PointF(104.762581f, 1074.99365f), - new PointF(104.758667f, 1075.01721f), - new PointF(104.757675f, 1075.04114f), - new PointF(104.759628f, 1075.065f), - new PointF(104.764488f, 1075.08838f), - new PointF(104.772186f, 1075.111f), - new PointF(104.782608f, 1075.13245f), - new PointF(104.782608f, 1075.13245f))); - } - } - - [Fact] - public void DoesNotThrowFillingTriangle() - { - using (Image image = new(28, 28)) - { - Polygon path = new( - new LinearLineSegment(new PointF(17.11f, 13.99659f), new PointF(14.01433f, 27.06201f)), - new LinearLineSegment(new PointF(14.01433f, 27.06201f), new PointF(13.79267f, 14.00023f)), - new LinearLineSegment(new PointF(13.79267f, 14.00023f), new PointF(17.11f, 13.99659f))); - - image.Mutate(ctx => ctx.Fill(Color.White, path)); - } - } - - [Fact] - public void DrawPathProcessor_UsesNonZeroRule_WhenStrokeNormalizationIsDisabled() - { - DrawingOptions options = new() - { - ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } - }; - - SolidPen pen = new(Color.Black, 3F) - { - StrokeOptions = { NormalizeOutput = false } - }; - - DrawPathProcessor processor = new(options, pen, new RectangularPolygon(2F, 2F, 8F, 8F)); - - using Image image = new(20, 20); - IImageProcessor pixelProcessor = - processor.CreatePixelSpecificProcessor(image.Configuration, image, image.Bounds); - - FillPathProcessor fillProcessor = Assert.IsType>(pixelProcessor); - FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); - - Assert.Equal(IntersectionRule.NonZero, definition.Options.ShapeOptions.IntersectionRule); - Assert.Equal( - RasterizerSamplingOrigin.PixelCenter, - definition.GetProtectedValue("SamplingOrigin")); - } - - [Fact] - public void DrawPathProcessor_PreservesRule_WhenStrokeNormalizationIsEnabled() - { - DrawingOptions options = new() - { - ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } - }; - - SolidPen pen = new(Color.Black, 3F) - { - StrokeOptions = { NormalizeOutput = true } - }; - - DrawPathProcessor processor = new(options, pen, new RectangularPolygon(2F, 2F, 8F, 8F)); - - using Image image = new(20, 20); - IImageProcessor pixelProcessor = - processor.CreatePixelSpecificProcessor(image.Configuration, image, image.Bounds); - - FillPathProcessor fillProcessor = Assert.IsType>(pixelProcessor); - FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); - - Assert.Equal(IntersectionRule.EvenOdd, definition.Options.ShapeOptions.IntersectionRule); - Assert.Equal( - RasterizerSamplingOrigin.PixelCenter, - definition.GetProtectedValue("SamplingOrigin")); - } - - [Fact] - public void FillPathProcessor_UsesConfiguredRasterizer() - { - RecordingRasterizer rasterizer = new(); - Configuration configuration = new(); - configuration.SetRasterizer(rasterizer); - - FillPathProcessor processor = new( - new DrawingOptions(), - Brushes.Solid(Color.White), - new EllipsePolygon(6F, 6F, 4F)); - - using Image image = new(configuration, 20, 20); - processor.Execute(configuration, image, image.Bounds); - - Assert.True(rasterizer.CallCount > 0); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void FillPathProcessor_UsesExpectedRasterizationModeAndPixelBoundarySamplingOrigin(bool antialias) - { - RecordingRasterizer rasterizer = new(); - Configuration configuration = new(); - configuration.SetRasterizer(rasterizer); - - DrawingOptions drawingOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = antialias - } - }; - - FillPathProcessor processor = new( - drawingOptions, - Brushes.Solid(Color.White), - new EllipsePolygon(6F, 6F, 4F)); - - using Image image = new(configuration, 20, 20); - processor.Execute(configuration, image, image.Bounds); - - RasterizationMode expectedMode = antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - Assert.Equal(expectedMode, rasterizer.LastRasterizationMode); - Assert.Equal(RasterizerSamplingOrigin.PixelBoundary, rasterizer.LastSamplingOrigin); - } - - private sealed class RecordingRasterizer : IRasterizer - { - public int CallCount { get; private set; } - - public RasterizationMode LastRasterizationMode { get; private set; } - - public RasterizerSamplingOrigin LastSamplingOrigin { get; private set; } - - public void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - this.CallCount++; - this.LastRasterizationMode = options.RasterizationMode; - this.LastSamplingOrigin = options.SamplingOrigin; - } - } -} - -internal static class ReflectionHelpers -{ - internal static T GetProtectedValue(this object obj, string name) - => (T)obj.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) - .Single(x => x.Name == name) - .GetValue(obj); - - internal static T GetPrivateFieldValue(this object obj, string name) - => (T)obj.GetType() - .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) - .Single(x => x.Name == name) - .GetValue(obj); -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs index fb141083f..a3d68f075 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs @@ -108,7 +108,11 @@ public void ApplyProcessors_ListOfProcessors_AppliesAllProcessorsToOperation() Assert.Contains(this.processorDefinition, operations.Applied.Select(x => x.NonGenericProcessor)); } - public void Dispose() => this.image.Dispose(); + public void Dispose() + { + this.image.Dispose(); + GC.SuppressFinalize(this); + } [Fact] public void GenericMutate_WhenDisposed_Throws() From 691bc17544c38608cf13fa85fd448fd88ee2ae11 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 17:55:31 +1000 Subject: [PATCH 072/136] Fix build --- .../DrawingCanvasBatcher{TPixel}.cs | 2 +- .../Drawing/EllipseStressTest.cs | 29 +++++++++--------- .../GraphicsOptionsTests.cs | 30 +++++++------------ .../SkiaCoverageDrawingBackendTests.cs | 6 ++-- ...sWithDrawingCanvasTests.GradientBrushes.cs | 2 +- .../TestUtilities/DebugDraw.cs | 12 +++++--- 6 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index c33021138..426e5be35 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -66,7 +66,7 @@ public void FlushCompositions() try { - CompositionScene scene = new(this.commands.ToArray()); + CompositionScene scene = new(this.commands); this.backend.FlushCompositions(this.configuration, this.targetFrame, scene); } finally diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs index 9c1272246..015a9f0e8 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs @@ -23,24 +23,23 @@ public class EllipseStressTest [Benchmark] public void DrawImageSharp() - { - for (int i = 0; i < 20_000; i++) - { - Color brushColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); - Color penColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); - - float r = this.Rand(20f) + 1f; - float x = this.Rand(this.width); - float y = this.Rand(this.height); - EllipsePolygon ellipse = new(new PointF(x, y), r); - this.image.Mutate( - m => m.ProcessWithCanvas(canvas => + => this.image.Mutate( + m => m.ProcessWithCanvas(canvas => + { + for (int i = 0; i < 20_000; i++) { + Color brushColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + Color penColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + + float r = this.Rand(20f) + 1f; + float x = this.Rand(this.width); + float y = this.Rand(this.height); + EllipsePolygon ellipse = new(new PointF(x, y), r); + canvas.Fill(ellipse, Brushes.Solid(brushColor)); canvas.Draw(Pens.Solid(penColor, this.Rand(5)), ellipse); - })); - } - } + } + })); [GlobalCleanup] public void Cleanup() diff --git a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs index 6700b36d9..77bd27e49 100644 --- a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs @@ -13,7 +13,7 @@ public class GraphicsOptionsTests private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); [Fact] - public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); + public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions); [Fact] public void DefaultGraphicsOptionsAntialias() @@ -25,25 +25,25 @@ public void DefaultGraphicsOptionsAntialias() [Fact] public void DefaultGraphicsOptionsBlendPercentage() { - const float Expected = 1F; - Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); + const float expected = 1F; + Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage); + Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage); } [Fact] public void DefaultGraphicsOptionsColorBlendingMode() { - const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; - Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); + const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal; + Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode); + Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); } [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { - const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; - Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); + const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode); + Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode); } [Fact] @@ -75,14 +75,4 @@ public void CloneIsDeep() Assert.NotEqual(expected, actual, GraphicsOptionsComparer); } - - [Fact] - public void IsOpaqueColor() - { - Assert.True(new GraphicsOptions().IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions { BlendPercentage = .5F }.IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions().IsOpaqueColorWithoutBlending(Color.Transparent)); - Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Lighten, BlendPercentage = 1F }.IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Normal, AlphaCompositionMode = PixelAlphaCompositionMode.DestOver, BlendPercentage = 1f }.IsOpaqueColorWithoutBlending(Color.Red)); - } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs index dc86d0b20..59010c027 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs @@ -32,7 +32,7 @@ public void DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage( Pen pen = Pens.Solid(Color.OrangeRed, 2F); using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + defaultImage.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen))); defaultImage.DebugSave( provider, "DefaultBackend_DrawText", @@ -42,7 +42,7 @@ public void DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage( using Image skiaBackendImage = provider.GetImage(); using SkiaCoverageDrawingBackend backend = new(); skiaBackendImage.Configuration.SetDrawingBackend(backend); - skiaBackendImage.Mutate(ctx => ctx.DrawText(drawingOptions, textOptions, text, brush, pen)); + skiaBackendImage.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen))); skiaBackendImage.DebugSave( provider, @@ -82,7 +82,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider ctx.DrawText(drawingOptions, textOptions, text, brush, pen: null)); + image.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen: null))); image.DebugSave( provider, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs index 7fbdc91f6..c69466242 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -584,7 +584,7 @@ public void FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground image.Mutate(ctx => { - ctx.Fill(Color.Red); + ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Red))); DrawingOptions glossOptions = new() { diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs index 53c15901b..6b027a7a0 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs @@ -32,7 +32,11 @@ public void Polygon(IPath path, float gridSize = 10f, float scale = 10f, [Caller gridSize *= scale; using Image img = new Image((int)(bounds.Right + (2 * gridSize)), (int)(bounds.Bottom + (2 * gridSize))); - img.Mutate(ctx => DrawGrid(ctx.Fill(TestBrush, path), bounds, gridSize)); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(path, TestBrush); + DrawGrid(canvas, bounds, gridSize); + })); string outDir = TestEnvironment.CreateOutputDirectory(this.outputDir); string outFile = System.IO.Path.Combine(outDir, testMethod + ".png"); @@ -41,18 +45,18 @@ public void Polygon(IPath path, float gridSize = 10f, float scale = 10f, [Caller private static PointF P(float x, float y) => new(x, y); - private static void DrawGrid(IImageProcessingContext ctx, RectangleF rect, float gridSize) + private static void DrawGrid(IDrawingCanvas canvas, RectangleF rect, float gridSize) { for (float x = rect.Left; x <= rect.Right; x += gridSize) { PointF[] line = [P(x, rect.Top), P(x, rect.Bottom)]; - ctx.DrawLine(GridPen, line); + canvas.DrawLine(GridPen, line); } for (float y = rect.Top; y <= rect.Bottom; y += gridSize) { PointF[] line = [P(rect.Left, y), P(rect.Right, y)]; - ctx.DrawLine(GridPen, line); + canvas.DrawLine(GridPen, line); } } } From 3fc0488fa5caf4ca72ddb5a037e4d63fc6dc2148 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 19:36:32 +1000 Subject: [PATCH 073/136] Update ImageSharp.Drawing.sln --- ImageSharp.Drawing.sln | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 318aeae96..c7e333c09 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -339,8 +339,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SixLabors.Fonts", "..\Fonts\src\SixLabors.Fonts\SixLabors.Fonts.csproj", "{4A922B77-34EC-EA6A-8E96-8353C8FA0640}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -367,10 +365,6 @@ Global {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A922B77-34EC-EA6A-8E96-8353C8FA0640}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -399,14 +393,12 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} - {4A922B77-34EC-EA6A-8E96-8353C8FA0640} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 - ..\Fonts\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{4a922b77-34ec-ea6a-8e96-8353c8fa0640}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection GlobalSection(Performance) = preSolution From f4a3b87b08a96f2e87bb5050e33a90e464511305 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 19:54:52 +1000 Subject: [PATCH 074/136] Include binaries use streaming for composition --- .../Backends/DefaultDrawingBackend.cs | 87 ++++++------------- .../ImageSharp.Drawing.Tests.csproj | 4 + 2 files changed, 30 insertions(+), 61 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 60d7a2599..625a4da14 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// -/// rasterizes one shared coverage map per batch and applies brushes in original command order. +/// rasterizes shared coverage scanlines per batch and applies brushes in original command order. /// /// /// @@ -177,15 +177,12 @@ internal void FlushPreparedBatch( } CompositionCoverageDefinition definition = compositionBatch.Definition; - using Buffer2D coverageMap = this.CreateCoverageMap(definition, configuration.MemoryAllocator); - Rectangle destinationBounds = destinationFrame.Rectangle; IReadOnlyList commands = compositionBatch.Commands; int commandCount = commands.Count; BrushApplicator[] applicators = new BrushApplicator[commandCount]; try { - int maxHeight = 0; for (int i = 0; i < commandCount; i++) { PreparedCompositionCommand command = commands[i]; @@ -195,17 +192,22 @@ internal void FlushPreparedBatch( command.GraphicsOptions, commandRegion, command.BrushBounds); - - if (command.DestinationRegion.Height > maxHeight) - { - maxHeight = command.DestinationRegion.Height; - } } - // Iterate by row so we slice the already-rasterized coverage map once per command row. - // We can do this in parallel since the applicators are thread-safe and each row is independent. - RowOperation operation = new(coverageMap, commands, applicators, destinationBounds, maxHeight); - ParallelRowIterator.IterateRows(configuration, destinationBounds, in operation); + // Stream composition directly from rasterizer scanlines so we do not allocate + // and then re-read an intermediate coverage map. + RowOperation operation = new( + commands, + applicators, + destinationBounds, + definition.RasterizerOptions.Interest.Top); + this.PrimaryRasterizer.Rasterize( + definition.Path, + definition.RasterizerOptions, + configuration.MemoryAllocator, + ref operation, + static (int y, Span scanline, ref RowOperation callbackState) => + callbackState.InvokeScanline(y, scanline)); } finally { @@ -216,80 +218,43 @@ internal void FlushPreparedBatch( } } - /// - /// Rasterizes one batch coverage map into a dense floating-point buffer. - /// - /// The path and rasterizer options shared by every command in the batch. - /// The allocator used for temporary coverage storage. - /// The populated coverage map for the batch interest region. - private Buffer2D CreateCoverageMap( - in CompositionCoverageDefinition definition, - MemoryAllocator allocator) - { - Size size = definition.RasterizerOptions.Interest.Size; - Buffer2D coverage = allocator.Allocate2D(size, AllocationOptions.Clean); - - (Buffer2D Buffer, int DestinationTop) state = (coverage, definition.RasterizerOptions.Interest.Top); - this.PrimaryRasterizer.Rasterize( - definition.Path, - definition.RasterizerOptions, - allocator, - ref state, - static (int y, Span scanline, ref (Buffer2D Buffer, int DestinationTop) callbackState) => - { - int row = y - callbackState.DestinationTop; - scanline.CopyTo(callbackState.Buffer.DangerousGetRowSpan(row)); - }); - - return coverage; - } - - private readonly struct RowOperation : IRowOperation + private readonly struct RowOperation where TPixel : unmanaged, IPixel { - private readonly Buffer2D coverageMap; private readonly IReadOnlyList commands; private readonly BrushApplicator[] applicators; private readonly Rectangle destinationBounds; - private readonly int maxHeight; + private readonly int coverageTop; public RowOperation( - Buffer2D coverageMap, IReadOnlyList commands, BrushApplicator[] applicators, Rectangle destinationBounds, - int maxHeight) + int coverageTop) { - this.coverageMap = coverageMap; this.commands = commands; this.applicators = applicators; this.destinationBounds = destinationBounds; - this.maxHeight = maxHeight; + this.coverageTop = coverageTop; } - public void Invoke(int y) + public void InvokeScanline(int y, Span scanline) { - if (y >= this.maxHeight) - { - return; - } - + int sourceY = y - this.coverageTop; for (int i = 0; i < this.commands.Count; i++) { PreparedCompositionCommand command = this.commands[i]; - if (y >= command.DestinationRegion.Height) + int commandY = sourceY - command.SourceOffset.Y; + if ((uint)commandY >= (uint)command.DestinationRegion.Height) { continue; } int destinationX = this.destinationBounds.X + command.DestinationRegion.X; - int destinationY = this.destinationBounds.Y + command.DestinationRegion.Y; + int destinationY = this.destinationBounds.Y + command.DestinationRegion.Y + commandY; int sourceStartX = command.SourceOffset.X; - int sourceStartY = command.SourceOffset.Y; - - Span rowCoverage = this.coverageMap.DangerousGetRowSpan(sourceStartY + y); - Span rowSlice = rowCoverage.Slice(sourceStartX, command.DestinationRegion.Width); - ApplyCoverageSpans(this.applicators[i], rowSlice, destinationX, destinationY + y); + Span rowSlice = scanline.Slice(sourceStartX, command.DestinationRegion.Width); + ApplyCoverageSpans(this.applicators[i], rowSlice, destinationX, destinationY); } } diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index b127c7141..9ceda1d1b 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -32,6 +32,10 @@ + + + + From 73e43471f1d9d10d8d26e8acc779c8150e0181cc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 20:02:14 +1000 Subject: [PATCH 075/136] Update ImageSharp.Drawing.Tests.csproj --- .../ImageSharp.Drawing.Tests.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 9ceda1d1b..9553c38f1 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -60,4 +60,13 @@ + + + + + + + + + From d8e09103274b62439db94d19fca10134642a89e4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 20:15:30 +1000 Subject: [PATCH 076/136] Use tolerance comparer --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 8 ++++++++ .../Processing/DrawingCanvasTests.RegionAndState.cs | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 914525ea8..8b624f85d 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -864,28 +864,36 @@ private static void DebugSaveBackendTriplet( $"{testName}_Default", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + cpuRegionImage.DebugSave( provider, $"{testName}_WebGPU_CPURegion", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + nativeSurfaceImage.DebugSave( provider, $"{testName}_WebGPU_NativeSurface", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(0.0003F); defaultImage.CompareToReferenceOutput( + tolerantComparer, provider, $"{testName}_Default", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + cpuRegionImage.CompareToReferenceOutput( + tolerantComparer, provider, $"{testName}_WebGPU_CPURegion", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + nativeSurfaceImage.CompareToReferenceOutput( + tolerantComparer, provider, $"{testName}_WebGPU_NativeSurface", appendPixelTypeToFileName: false, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs index c79ff558c..8a1fd82a2 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -59,7 +60,9 @@ public void SaveRestore_ClipPath_MatchesReference(TestImageProvider Date: Wed, 4 Mar 2026 20:20:44 +1000 Subject: [PATCH 077/136] Update DrawingCanvasTests.RegionAndState.cs --- .../Processing/DrawingCanvasTests.RegionAndState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs index 8a1fd82a2..435069a8a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -62,7 +62,7 @@ public void SaveRestore_ClipPath_MatchesReference(TestImageProvider Date: Wed, 4 Mar 2026 20:48:34 +1000 Subject: [PATCH 078/136] Skip WebGPU drawing tests on Linux --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 8b624f85d..eb62a072a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#if !OS_Linux +// WebGPU is failing in our CI environment in Ubuntu with +// WebGPU adapter request failed with status 'Unavailable' using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -970,3 +973,4 @@ private static Buffer2DRegion GetFrameRegion(Image image where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); } +#endif From 2ec62848aaf8b3cb96d254e37b812b9e2093944d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 20:54:57 +1000 Subject: [PATCH 079/136] Skip WebGPU tests for all CI --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index eb62a072a..7a099bd68 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#if !OS_Linux +#if !ENV_CI // WebGPU is failing in our CI environment in Ubuntu with // WebGPU adapter request failed with status 'Unavailable' +// It's also failing in Windows CI with "Test host process crashed : Fatal error.0xC0000005" +// TODO: Ask the Silk.NET team for help. using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; From f427d0448c1f0e1e34e2a85993b718c594d7c92f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Mar 2026 21:50:23 +1000 Subject: [PATCH 080/136] Remove rasterizer config --- .../Backends/DefaultDrawingBackend.cs | 30 +----- .../RasterizerDefaultsExtensions.cs | 101 +----------------- .../Shapes/Rasterization/DefaultRasterizer.cs | 6 +- .../Shapes/Rasterization/IRasterizer.cs | 43 -------- .../RasterizerScanlineHandler{TState}.cs | 14 +++ .../Rasterization/ScanlineRasterizer.cs | 6 +- .../Drawing/DrawPolygon.cs | 31 +----- ...rocessWithDrawingCanvasTests.Robustness.cs | 7 +- .../RasterizerDefaultsExtensionsTests.cs | 62 ----------- ...cs => DefaultRasterizerRegressionTests.cs} | 4 +- .../Shapes/Scan/DefaultRasterizerTests.cs | 14 ++- 11 files changed, 43 insertions(+), 275 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs rename tests/ImageSharp.Drawing.Tests/Shapes/Scan/{SharpBlazeRasterizerTests.cs => DefaultRasterizerRegressionTests.cs} (96%) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 625a4da14..df9168bfb 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -36,36 +36,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed class DefaultDrawingBackend : IDrawingBackend { - /// - /// Initializes a new instance of the class. - /// - /// Rasterizer used for coverage generation. - private DefaultDrawingBackend(IRasterizer primaryRasterizer) - { - Guard.NotNull(primaryRasterizer, nameof(primaryRasterizer)); - this.PrimaryRasterizer = primaryRasterizer; - } - /// /// Gets the default backend instance. /// - public static DefaultDrawingBackend Instance { get; } = new(DefaultRasterizer.Instance); - - /// - /// Gets the primary rasterizer used by this backend. - /// - public IRasterizer PrimaryRasterizer { get; } - - /// - /// Creates a backend that uses the given rasterizer as the primary implementation. - /// - /// Primary rasterizer. - /// A backend instance. - public static DefaultDrawingBackend Create(IRasterizer rasterizer) - { - Guard.NotNull(rasterizer, nameof(rasterizer)); - return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new DefaultDrawingBackend(rasterizer); - } + public static DefaultDrawingBackend Instance { get; } = new(); /// public bool IsCompositionBrushSupported(Brush brush) @@ -201,7 +175,7 @@ internal void FlushPreparedBatch( applicators, destinationBounds, definition.RasterizerOptions.Interest.Top); - this.PrimaryRasterizer.Rasterize( + DefaultRasterizer.Instance.Rasterize( definition.Path, definition.RasterizerOptions, configuration.MemoryAllocator, diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs index 510101e10..343dc9069 100644 --- a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -2,12 +2,11 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Adds extensions that allow configuring the path rasterizer implementation. +/// Adds extensions that allow configuring the drawing backend implementation. /// internal static class RasterizerDefaultsExtensions { @@ -22,11 +21,6 @@ internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingC Guard.NotNull(backend, nameof(backend)); context.Properties[typeof(IDrawingBackend)] = backend; - if (backend is DefaultDrawingBackend defaultBackend) - { - context.Properties[typeof(IRasterizer)] = defaultBackend.PrimaryRasterizer; - } - return context; } @@ -39,11 +33,6 @@ internal static void SetDrawingBackend(this Configuration configuration, IDrawin { Guard.NotNull(backend, nameof(backend)); configuration.Properties[typeof(IDrawingBackend)] = backend; - - if (backend is DefaultDrawingBackend defaultBackend) - { - configuration.Properties[typeof(IRasterizer)] = defaultBackend.PrimaryRasterizer; - } } /// @@ -59,12 +48,6 @@ internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext c return configured; } - if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && - rasterizer is IRasterizer configuredRasterizer) - { - return DefaultDrawingBackend.Create(configuredRasterizer); - } - return context.Configuration.GetDrawingBackend(); } @@ -81,90 +64,8 @@ internal static IDrawingBackend GetDrawingBackend(this Configuration configurati return configured; } - if (configuration.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && - rasterizer is IRasterizer configuredRasterizer) - { - IDrawingBackend rasterizerBackend = DefaultDrawingBackend.Create(configuredRasterizer); - configuration.Properties[typeof(IDrawingBackend)] = rasterizerBackend; - return rasterizerBackend; - } - IDrawingBackend defaultBackend = DefaultDrawingBackend.Instance; configuration.Properties[typeof(IDrawingBackend)] = defaultBackend; return defaultBackend; } - - /// - /// Sets the rasterizer against the source image processing context. - /// - /// The image processing context to store the rasterizer against. - /// The rasterizer to use. - /// The passed in to allow chaining. - internal static IImageProcessingContext SetRasterizer(this IImageProcessingContext context, IRasterizer rasterizer) - { - Guard.NotNull(rasterizer, nameof(rasterizer)); - context.Properties[typeof(IRasterizer)] = rasterizer; - context.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Create(rasterizer); - return context; - } - - /// - /// Sets the default rasterizer against the configuration. - /// - /// The configuration to store the rasterizer against. - /// The rasterizer to use. - internal static void SetRasterizer(this Configuration configuration, IRasterizer rasterizer) - { - Guard.NotNull(rasterizer, nameof(rasterizer)); - configuration.Properties[typeof(IRasterizer)] = rasterizer; - configuration.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Create(rasterizer); - } - - /// - /// Gets the rasterizer from the source image processing context. - /// - /// The image processing context to retrieve the rasterizer from. - /// The configured rasterizer. - internal static IRasterizer GetRasterizer(this IImageProcessingContext context) - { - if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && - rasterizer is IRasterizer configured) - { - return configured; - } - - if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is DefaultDrawingBackend defaultBackend) - { - return defaultBackend.PrimaryRasterizer; - } - - // Do not cache config fallback in the context so changes on configuration reflow. - return context.Configuration.GetRasterizer(); - } - - /// - /// Gets the default rasterizer from the configuration. - /// - /// The configuration to retrieve the rasterizer from. - /// The configured rasterizer. - internal static IRasterizer GetRasterizer(this Configuration configuration) - { - if (configuration.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && - rasterizer is IRasterizer configured) - { - return configured; - } - - if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is DefaultDrawingBackend defaultBackend) - { - return defaultBackend.PrimaryRasterizer; - } - - IRasterizer defaultRasterizer = DefaultRasterizer.Instance; - configuration.Properties[typeof(IRasterizer)] = defaultRasterizer; - configuration.Properties[typeof(IDrawingBackend)] = DefaultDrawingBackend.Instance; - return defaultRasterizer; - } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs index 2544ff5d9..23da79a4a 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs @@ -13,14 +13,16 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// area/cover scanning and chooses an internal execution strategy (parallel row-tiles when /// profitable, sequential fallback otherwise). /// -internal sealed class DefaultRasterizer : IRasterizer +internal sealed class DefaultRasterizer { /// /// Gets the singleton default rasterizer instance. /// public static DefaultRasterizer Instance { get; } = new(); - /// + /// + /// Rasterizes the path into scanline coverage. + /// public void Rasterize( IPath path, in RasterizerOptions options, diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs deleted file mode 100644 index af642517a..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Delegate invoked for each rasterized scanline. -/// -/// The caller-provided state type. -/// The destination y coordinate. -/// Coverage values for the scanline. -/// Caller-provided mutable state. -internal delegate void RasterizerScanlineHandler(int y, Span scanline, ref TState state) - where TState : struct; - -/// -/// Defines a rasterizer capable of converting vector paths into per-pixel scanline coverage. -/// -internal interface IRasterizer -{ - /// - /// Rasterizes a path into scanline coverage and invokes - /// for each non-empty destination row. - /// - /// The caller-provided state type. - /// The path to rasterize. - /// Rasterization options. - /// The memory allocator used for temporary buffers. - /// Caller-provided mutable state passed to the callback. - /// - /// Callback invoked for each rasterized scanline. Implementations should invoke this callback - /// in ascending y order and not concurrently for a single invocation. - /// - void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct; -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs new file mode 100644 index 000000000..6cd07f57a --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Delegate invoked for each rasterized scanline. +/// +/// The caller-provided state type. +/// The destination y coordinate. +/// Coverage values for the scanline. +/// Caller-provided mutable state. +internal delegate void RasterizerScanlineHandler(int y, Span scanline, ref TState state) + where TState : struct; diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs index 6a2183c0b..d26a49847 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs @@ -13,14 +13,16 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// It is retained as a compact fallback/reference implementation and as an explicit /// non-tiled option for profiling and comparison. /// -internal sealed class ScanlineRasterizer : IRasterizer +internal sealed class ScanlineRasterizer { /// /// Gets the singleton scanline rasterizer instance. /// public static ScanlineRasterizer Instance { get; } = new(); - /// + /// + /// Rasterizes the path into scanline coverage using the sequential scanner path. + /// public void Rasterize( IPath path, in RasterizerOptions options, diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index b47dbf640..6c6b1dd98 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -9,7 +9,6 @@ using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -164,34 +163,8 @@ public void Cleanup() public void SystemDrawing() => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); - // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. [Benchmark] - public void ImageSharpCombinedPathsScanlineRasterizer() - => this.image.Mutate(c => - { - c.SetRasterizer(ScanlineRasterizer.Instance); - c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath)); - }); - - [Benchmark] - public void ImageSharpSeparatePathsScanlineRasterizer() - => this.image.Mutate( - c => - { - // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. - c.SetRasterizer(ScanlineRasterizer.Instance); - c.ProcessWithCanvas(canvas => - { - foreach (PointF[] loop in this.points) - { - canvas.Draw(Processing.Pens.Solid(Color.White, this.Thickness), new Polygon(loop)); - } - }); - }); - - // Tiled is now the framework default rasterizer path. - [Benchmark] - public void ImageSharpCombinedPathsTiled() + public void ImageSharpCombinedPaths() => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] @@ -199,7 +172,7 @@ public void ImageSharpCombinedPathsWebGPUBackend() => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark] - public void ImageSharpSeparatePathsTiled() + public void ImageSharpSeparatePaths() => this.image.Mutate( c => c.ProcessWithCanvas(canvas => { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs index 9a8b45be2..cd047bfe6 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs @@ -8,7 +8,6 @@ using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -327,11 +326,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider using Image image = provider.GetImage(); - image.Mutate(c => - { - c.SetRasterizer(DefaultRasterizer.Instance); - c.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(Color.White, thickness), path)); - }); + image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(Color.White, thickness), path))); image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 88c9f54e8..e62d612cf 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -5,25 +5,12 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public class RasterizerDefaultsExtensionsTests { - [Fact] - public void GetDefaultRasterizerFromConfiguration_AlwaysReturnsDefaultInstance() - { - Configuration configuration = new(); - - IRasterizer first = configuration.GetRasterizer(); - IRasterizer second = configuration.GetRasterizer(); - - Assert.Same(first, second); - Assert.Same(DefaultRasterizer.Instance, first); - } - [Fact] public void GetDefaultDrawingBackendFromConfiguration_AlwaysReturnsDefaultInstance() { @@ -36,42 +23,6 @@ public void GetDefaultDrawingBackendFromConfiguration_AlwaysReturnsDefaultInstan Assert.Same(DefaultDrawingBackend.Instance, first); } - [Fact] - public void SetRasterizerOnConfiguration_RoundTrips() - { - Configuration configuration = new(); - RecordingRasterizer rasterizer = new(); - - configuration.SetRasterizer(rasterizer); - - Assert.Same(rasterizer, configuration.GetRasterizer()); - Assert.IsType(configuration.GetDrawingBackend()); - } - - [Fact] - public void SetRasterizerOnProcessingContext_RoundTrips() - { - Configuration configuration = new(); - FakeImageOperationsProvider.FakeImageOperations context = new(configuration, null, true); - RecordingRasterizer rasterizer = new(); - - context.SetRasterizer(rasterizer); - - Assert.Same(rasterizer, context.GetRasterizer()); - Assert.IsType(context.GetDrawingBackend()); - } - - [Fact] - public void GetRasterizerFromProcessingContext_FallsBackToConfiguration() - { - Configuration configuration = new(); - RecordingRasterizer rasterizer = new(); - configuration.SetRasterizer(rasterizer); - FakeImageOperationsProvider.FakeImageOperations context = new(configuration, null, true); - - Assert.Same(rasterizer, context.GetRasterizer()); - } - [Fact] public void SetDrawingBackendOnConfiguration_RoundTrips() { @@ -95,19 +46,6 @@ public void SetDrawingBackendOnProcessingContext_RoundTrips() Assert.Same(backend, context.GetDrawingBackend()); } - private sealed class RecordingRasterizer : IRasterizer - { - public void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - } - } - private sealed class RecordingDrawingBackend : IDrawingBackend { public bool IsCompositionBrushSupported(Brush brush) diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs similarity index 96% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs rename to tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs index 19842362c..9b9059425 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; -public class SharpBlazeRasterizerTests +public class DefaultRasterizerRegressionTests { [Fact] public void EmitsCoverageForSubpixelThinRectangle() @@ -74,7 +74,7 @@ void Rasterize() => Assert.Contains("too large", exception.Message); } - private static float[] Rasterize(IRasterizer rasterizer, IPath path, in RasterizerOptions options) + private static float[] Rasterize(DefaultRasterizer rasterizer, IPath path, in RasterizerOptions options) { int width = options.Interest.Width; int height = options.Interest.Height; diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs index 382d6fa86..eb9a28064 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs @@ -56,7 +56,19 @@ public void MatchesDefaultRasterizer_ForPixelCenterSampling() AssertCoverageEqual(expected, actual); } - private static float[] Rasterize(IRasterizer rasterizer, IPath path, in RasterizerOptions options) + private static float[] Rasterize(DefaultRasterizer rasterizer, IPath path, in RasterizerOptions options) + { + int width = options.Interest.Width; + int height = options.Interest.Height; + float[] coverage = new float[width * height]; + CaptureState state = new(coverage, width, options.Interest.Top); + + rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + + return coverage; + } + + private static float[] Rasterize(ScanlineRasterizer rasterizer, IPath path, in RasterizerOptions options) { int width = options.Interest.Width; int height = options.Interest.Height; From d427d29fbb89910072727113c467dd19923a018e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 10:39:07 +1000 Subject: [PATCH 081/136] Replace PolygonScanner with DefaultRasterizer and optimize --- .../Processing/Backends/CompositionCommand.cs | 59 +- .../Backends/CompositionScenePlanner.cs | 61 +- .../Backends/DefaultDrawingBackend.cs | 160 +- .../Shapes/Rasterization/DefaultRasterizer.cs | 2162 +++++++++++++++- .../Shapes/Rasterization/PolygonScanner.cs | 2292 ----------------- .../Shapes/Rasterization/PolygonScanning.MD | 6 +- .../RasterizerCoverageRowHandler.cs | 12 + .../RasterizerScanlineHandler{TState}.cs | 14 - .../Rasterization/ScanlineRasterizer.cs | 46 - .../Scan/DefaultRasterizerRegressionTests.cs | 85 +- .../Shapes/Scan/DefaultRasterizerTests.cs | 54 +- tests/coverlet.runsettings | 4 +- 12 files changed, 2303 insertions(+), 2652 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index 67d4520d8..c5c3be603 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -116,64 +116,23 @@ public static CompositionCommand Create( } /// - /// Computes a coverage definition key from path geometry and rasterization state. + /// Computes a coverage definition key from path identity and rasterization state. /// /// Path to rasterize. /// Rasterizer options used for coverage generation. - /// Optional scoped cache keyed by path identity to avoid repeated path flattening. + /// Unused. Retained for API compatibility. /// A stable key for coverage-equivalent commands. public static int ComputeCoverageDefinitionKey( IPath path, in RasterizerOptions rasterizerOptions, Dictionary? definitionKeyCache = null) { - // Fast path: when the caller provides a cache and the same IPath object is - // reused (e.g. cached glyph sub-pixel variants), skip the expensive - // Flatten + point-hash and return the cached key. - if (definitionKeyCache is not null) - { - int pathIdentity = RuntimeHelpers.GetHashCode(path); - int rasterState = HashCode.Combine( - rasterizerOptions.Interest.Size, - (int)rasterizerOptions.IntersectionRule, - (int)rasterizerOptions.RasterizationMode, - (int)rasterizerOptions.SamplingOrigin); - int cacheProbe = HashCode.Combine(pathIdentity, rasterState); - - if (definitionKeyCache.TryGetValue(cacheProbe, out (IPath Path, int RasterState, int DefinitionKey) cached) && - ReferenceEquals(cached.Path, path) && - cached.RasterState == rasterState) - { - return cached.DefinitionKey; - } - - int definitionKey = ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions); - definitionKeyCache[cacheProbe] = (path, rasterState, definitionKey); - return definitionKey; - } - - return ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions); - } - - private static int ComputeCoverageDefinitionKeySlow(IPath path, in RasterizerOptions rasterizerOptions) - { - HashCode hash = default; - foreach (ISimplePath simplePath in path.Flatten()) - { - ReadOnlySpan points = simplePath.Points.Span; - hash.Add(simplePath.IsClosed); - hash.Add(points.Length); - for (int i = 0; i < points.Length; i++) - { - hash.Add(points[i].X); - hash.Add(points[i].Y); - } - } - - hash.Add(rasterizerOptions.Interest.Size); - hash.Add((int)rasterizerOptions.IntersectionRule); - hash.Add((int)rasterizerOptions.RasterizationMode); - hash.Add((int)rasterizerOptions.SamplingOrigin); - return hash.ToHashCode(); + int pathIdentity = RuntimeHelpers.GetHashCode(path); + int rasterState = HashCode.Combine( + rasterizerOptions.Interest.Size, + (int)rasterizerOptions.IntersectionRule, + (int)rasterizerOptions.RasterizationMode, + (int)rasterizerOptions.SamplingOrigin); + return HashCode.Combine(pathIdentity, rasterState); } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs index e337d54c8..6cfe07d8b 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// @@ -18,14 +20,16 @@ public static List CreatePreparedBatches( IReadOnlyList commands, in Rectangle targetBounds) { - List batches = []; + int commandCount = commands.Count; + List batches = new(EstimateBatchCapacity(commandCount)); int index = 0; - while (index < commands.Count) + while (index < commandCount) { CompositionCommand definitionCommand = commands[index]; int definitionKey = definitionCommand.DefinitionKey; - List preparedCommands = []; - for (; index < commands.Count; index++) + int remainingCount = commandCount - index; + List preparedCommands = new(EstimatePreparedCommandCapacity(remainingCount)); + for (; index < commandCount; index++) { CompositionCommand command = commands[index]; if (command.DefinitionKey != definitionKey) @@ -56,6 +60,55 @@ public static List CreatePreparedBatches( return batches; } + /// + /// Estimates initial capacity for the outer batch list from total scene command count. + /// + /// Total number of scene commands. + /// Suggested initial capacity for the batch list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EstimateBatchCapacity(int commandCount) + { + // Typical scenes reuse coverage definitions, so batch count is usually + // meaningfully lower than command count. + if (commandCount <= 8) + { + return commandCount; + } + + if (commandCount <= 128) + { + return commandCount / 2; + } + + return commandCount / 4; + } + + /// + /// Estimates initial capacity for one contiguous prepared-command run. + /// + /// Commands remaining from the current scan index. + /// Suggested initial capacity for the current prepared-command list. + /// + /// This estimate is intentionally capped for large tails because the list is + /// allocated per run during scanning rather than once per scene. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EstimatePreparedCommandCapacity(int remainingCount) + { + // Most adjacent commands share a definition in small-medium scenes. + if (remainingCount <= 16) + { + return remainingCount; + } + + if (remainingCount <= 128) + { + return remainingCount / 2; + } + + return 64; + } + /// /// Clips one scene command to target bounds and computes coverage source offset mapping. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index df9168bfb..9cefb8068 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -2,8 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -175,13 +173,12 @@ internal void FlushPreparedBatch( applicators, destinationBounds, definition.RasterizerOptions.Interest.Top); - DefaultRasterizer.Instance.Rasterize( + + DefaultRasterizer.RasterizeRows( definition.Path, definition.RasterizerOptions, configuration.MemoryAllocator, - ref operation, - static (int y, Span scanline, ref RowOperation callbackState) => - callbackState.InvokeScanline(y, scanline)); + operation.InvokeCoverageRow); } finally { @@ -212,153 +209,40 @@ public RowOperation( this.coverageTop = coverageTop; } - public void InvokeScanline(int y, Span scanline) + public void InvokeCoverageRow(int y, int startX, Span coverage) { int sourceY = y - this.coverageTop; + int rowStart = startX; + int rowEnd = startX + coverage.Length; + + Rectangle destinationBounds = this.destinationBounds; + BrushApplicator[] applicators = this.applicators; for (int i = 0; i < this.commands.Count; i++) { PreparedCompositionCommand command = this.commands[i]; + Rectangle commandDestination = command.DestinationRegion; + int commandY = sourceY - command.SourceOffset.Y; - if ((uint)commandY >= (uint)command.DestinationRegion.Height) + if ((uint)commandY >= (uint)commandDestination.Height) { continue; } - int destinationX = this.destinationBounds.X + command.DestinationRegion.X; - int destinationY = this.destinationBounds.Y + command.DestinationRegion.Y + commandY; int sourceStartX = command.SourceOffset.X; - Span rowSlice = scanline.Slice(sourceStartX, command.DestinationRegion.Width); - ApplyCoverageSpans(this.applicators[i], rowSlice, destinationX, destinationY); - } - } - - /// - /// Applies only contiguous non-zero coverage spans for a scanline. - /// - /// Brush applicator used to composite pixels. - /// Scanline coverage values for the current command row. - /// Destination x coordinate for the start of . - /// Destination y coordinate for the scanline. - private static void ApplyCoverageSpans( - BrushApplicator applicator, - Span coverage, - int destinationX, - int destinationY) - { - // Use SIMD path when available and the span is large enough to amortize setup. - if (Vector.IsHardwareAccelerated && coverage.Length >= (Vector.Count * 2)) - { - ApplyCoverageSpansSimd(applicator, coverage, destinationX, destinationY); - return; - } - - ApplyCoverageSpansScalar(applicator, coverage, destinationX, destinationY); - } - - /// - /// Applies contiguous non-zero coverage spans using SIMD-accelerated zero/non-zero chunk checks. - /// - /// Brush applicator used to composite pixels. - /// Scanline coverage values for the current command row. - /// Destination x coordinate for the start of . - /// Destination y coordinate for the scanline. - private static void ApplyCoverageSpansSimd( - BrushApplicator applicator, - Span coverage, - int destinationX, - int destinationY) - { - int i = 0; - int n = coverage.Length; - int width = Vector.Count; - Vector zero = Vector.Zero; - - while (i < n) - { - // Phase 1: skip fully-zero SIMD blocks. - while (i <= n - width) - { - Vector v = new(coverage.Slice(i, width)); - if (!Vector.EqualsAll(v, zero)) - { - break; - } - - i += width; - } - - while (i < n && coverage[i] == 0F) - { - i++; - } - - if (i >= n) - { - return; - } - - int runStart = i; - - // Phase 2: advance across fully non-zero SIMD blocks. - while (i <= n - width) + int sourceEndX = sourceStartX + commandDestination.Width; + int overlapStart = Math.Max(rowStart, sourceStartX); + int overlapEnd = Math.Min(rowEnd, sourceEndX); + if (overlapEnd <= overlapStart) { - Vector v = new(coverage.Slice(i, width)); - Vector eqZero = Vector.Equals(v, zero); - if (!Vector.EqualsAll(eqZero, Vector.Zero)) - { - break; - } - - i += width; + continue; } - while (i < n && coverage[i] != 0F) - { - i++; - } + int localStart = overlapStart - rowStart; + int localLength = overlapEnd - overlapStart; + int destinationX = destinationBounds.X + commandDestination.X + (overlapStart - sourceStartX); + int destinationY = destinationBounds.Y + commandDestination.Y + commandY; - // Apply exactly one contiguous non-zero run. - applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY); - } - } - - /// - /// Applies contiguous non-zero coverage spans using a scalar scan. - /// - /// Brush applicator used to composite pixels. - /// Scanline coverage values for the current command row. - /// Destination x coordinate for the start of . - /// Destination y coordinate for the scanline. - private static void ApplyCoverageSpansScalar( - BrushApplicator applicator, - Span coverage, - int destinationX, - int destinationY) - { - // Track the start of a contiguous non-zero coverage run. - int runStart = -1; - for (int i = 0; i < coverage.Length; i++) - { - if (coverage[i] > 0F) - { - // Enter a new run when transitioning from zero to non-zero coverage. - if (runStart < 0) - { - runStart = i; - } - } - else if (runStart >= 0) - { - // Coverage returned to zero: apply the finished run only. - applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY); - runStart = -1; - } - } - - if (runStart >= 0) - { - // Flush trailing run that reaches end-of-scanline. - applicator.Apply(coverage[runStart..], destinationX + runStart, destinationY); + applicators[i].Apply(coverage.Slice(localStart, localLength), destinationX, destinationY); } } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs index 23da79a4a..3c4aa61ad 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs @@ -1,46 +1,2172 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// -/// Default CPU rasterizer. +/// Default fixed-point rasterizer that converts polygon edges into per-row coverage. /// /// -/// This rasterizer delegates to , which performs fixed-point -/// area/cover scanning and chooses an internal execution strategy (parallel row-tiles when -/// profitable, sequential fallback otherwise). +/// The scanner has two execution modes: +/// 1. Parallel tiled execution (default): build an edge table once, bucket edges by tile rows, +/// rasterize tiles in parallel with worker-local scratch, then emit covered rows directly. +/// 2. Sequential execution: reuse the same edge table and process band buckets on one thread. +/// +/// Both modes share the same coverage math and fill-rule handling, ensuring consistent +/// scan conversion regardless of scheduling strategy. /// -internal sealed class DefaultRasterizer +internal static class DefaultRasterizer { + // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). + // Keeping this bounded prevents pathological full-image allocations on very large interests. + private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; + + // Tile height used by the parallel row-tiling pipeline. + private const int DefaultTileHeight = 16; + + // Cap worker fan-out for coverage emission + composition callbacks. + // Higher counts increased scheduling overhead for medium geometry workloads. + private const int MaxParallelWorkerCount = 12; + + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private static readonly int WordBitCount = nint.Size * 8; + private const int AreaToCoverageShift = 9; + private const int CoverageStepCount = 256; + private const int EvenOddMask = (CoverageStepCount * 2) - 1; + private const int EvenOddPeriod = CoverageStepCount * 2; + private const float CoverageScale = 1F / CoverageStepCount; + + /// + /// Rasterizes the path into trimmed coverage rows using the default execution policy. + /// + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + public static void RasterizeRows( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler) + => RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: true); + /// - /// Gets the singleton default rasterizer instance. + /// Rasterizes the path into trimmed coverage rows using forced sequential execution. /// - public static DefaultRasterizer Instance { get; } = new(); + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + public static void RasterizeRowsSequential( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler) + => RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: false); /// - /// Rasterizes the path into scanline coverage. + /// Shared entry point for trimmed-row rasterization. /// - public void Rasterize( + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + /// + /// If , the scanner may use parallel tiled execution when profitable. + /// + private static void RasterizeCoreRows( IPath path, in RasterizerOptions options, MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct + RasterizerCoverageRowHandler rowHandler, + bool allowParallel) { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); - Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); - Rectangle interest = options.Interest; - if (interest.Equals(Rectangle.Empty)) + int width = interest.Width; + int height = interest.Height; + if (width <= 0 || height <= 0) + { + return; + } + + int wordsPerRow = BitVectorsForMaxBitCount(width); + int maxBandRows = 0; + long coverStride = (long)width * 2; + if (coverStride > int.MaxValue || !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) + { + ThrowInterestBoundsTooLarge(); + } + + int coverStrideInt = (int)coverStride; + bool samplePixelCenter = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + + using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); + using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); + int edgeCount = BuildEdgeTable( + multipolygon, + interest.Left, + interest.Top, + height, + samplingOffsetX, + samplingOffsetY, + edgeDataOwner.Memory.Span); + if (edgeCount <= 0) + { + return; + } + + if (allowParallel && + TryRasterizeParallel( + edgeDataOwner.Memory, + edgeCount, + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + allocator, + rowHandler)) + { + return; + } + + RasterizeSequentialBands( + edgeDataOwner.Memory.Span[..edgeCount], + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + allocator, + rowHandler); + } + + /// + /// Sequential implementation using band buckets over the prebuilt edge table. + /// + /// Prebuilt edges in scanner-local coordinates. + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Maximum rows per reusable scratch band. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + private static void RasterizeSequentialBands( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStrideInt, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler) + { + int bandHeight = maxBandRows; + int bandCount = (height + bandHeight - 1) / bandHeight; + if (bandCount < 1) { return; } - PolygonScanner.Rasterize(path, options, allocator, ref state, scanlineHandler); + using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span bandCounts = bandCountsOwner.Memory.Span; + long totalBandEdgeReferences = 0; + for (int i = 0; i < edges.Length; i++) + { + // Each edge can overlap multiple bands. We first count references so we can build + // a compact contiguous index list (CSR-style) without per-band allocations. + int startBand = edges[i].MinRow / bandHeight; + int endBand = edges[i].MaxRow / bandHeight; + totalBandEdgeReferences += (endBand - startBand) + 1; + if (totalBandEdgeReferences > int.MaxValue) + { + ThrowInterestBoundsTooLarge(); + } + + for (int b = startBand; b <= endBand; b++) + { + bandCounts[b]++; + } + } + + int totalReferences = (int)totalBandEdgeReferences; + using IMemoryOwner bandOffsetsOwner = allocator.Allocate(bandCount + 1); + Span bandOffsets = bandOffsetsOwner.Memory.Span; + int offset = 0; + for (int b = 0; b < bandCount; b++) + { + // Prefix sum: bandOffsets[b] is the start index of band b inside bandEdgeReferences. + bandOffsets[b] = offset; + offset += bandCounts[b]; + } + + bandOffsets[bandCount] = offset; + using IMemoryOwner bandWriteCursorOwner = allocator.Allocate(bandCount); + Span bandWriteCursor = bandWriteCursorOwner.Memory.Span; + bandOffsets[..bandCount].CopyTo(bandWriteCursor); + + using IMemoryOwner bandEdgeReferencesOwner = allocator.Allocate(totalReferences); + Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; + for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) + { + // Scatter each edge index to all bands touched by its row range. + int startBand = edges[edgeIndex].MinRow / bandHeight; + int endBand = edges[edgeIndex].MaxRow / bandHeight; + for (int b = startBand; b <= endBand; b++) + { + bandEdgeReferences[bandWriteCursor[b]++] = edgeIndex; + } + } + + using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); + for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) + { + int bandTop = bandIndex * bandHeight; + int currentBandHeight = Math.Min(bandHeight, height - bandTop); + int start = bandOffsets[bandIndex]; + int length = bandOffsets[bandIndex + 1] - start; + if (length == 0) + { + // No edge crosses this band, so there is nothing to rasterize or clear. + continue; + } + + Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); + ReadOnlySpan bandEdges = bandEdgeReferences.Slice(start, length); + context.RasterizeEdgeTable(edges, bandEdges, bandTop); + context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } + } + + /// + /// Attempts to execute the tiled parallel scanner. + /// + /// Memory block containing prebuilt edges. + /// Number of valid edges in . + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Maximum rows per worker scratch context. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + /// + /// when the tiled path executed successfully; + /// when the caller should run sequential fallback. + /// + private static bool TryRasterizeParallel( + Memory edgeMemory, + int edgeCount, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler) + { + int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); + if (tileHeight < 1) + { + return false; + } + + int tileCount = (height + tileHeight - 1) / tileHeight; + if (tileCount == 1) + { + // Tiny workload fast path: avoid bucket construction and worker scheduling + // when everything fits in a single tile. + RasterizeSingleTileDirect( + edgeMemory.Span[..edgeCount], + width, + height, + interestTop, + wordsPerRow, + coverStride, + intersectionRule, + rasterizationMode, + allocator, + rowHandler); + + return true; + } + + if (Environment.ProcessorCount < 2) + { + return false; + } + + using IMemoryOwner tileCountsOwner = allocator.Allocate(tileCount, AllocationOptions.Clean); + Span tileCounts = tileCountsOwner.Memory.Span; + + long totalTileEdgeReferences = 0; + Span edgeBuffer = edgeMemory.Span; + for (int i = 0; i < edgeCount; i++) + { + // Same CSR construction as sequential mode, now keyed by tile instead of band. + int startTile = edgeBuffer[i].MinRow / tileHeight; + int endTile = edgeBuffer[i].MaxRow / tileHeight; + int tileSpan = (endTile - startTile) + 1; + totalTileEdgeReferences += tileSpan; + + if (totalTileEdgeReferences > int.MaxValue) + { + return false; + } + + for (int t = startTile; t <= endTile; t++) + { + tileCounts[t]++; + } + } + + int totalReferences = (int)totalTileEdgeReferences; + using IMemoryOwner tileOffsetsOwner = allocator.Allocate(tileCount + 1); + Memory tileOffsetsMemory = tileOffsetsOwner.Memory; + Span tileOffsets = tileOffsetsMemory.Span; + + int offset = 0; + for (int t = 0; t < tileCount; t++) + { + // Prefix sum over tile counts so each tile gets one contiguous slice. + tileOffsets[t] = offset; + offset += tileCounts[t]; + } + + tileOffsets[tileCount] = offset; + using IMemoryOwner tileWriteCursorOwner = allocator.Allocate(tileCount); + Span tileWriteCursor = tileWriteCursorOwner.Memory.Span; + tileOffsets[..tileCount].CopyTo(tileWriteCursor); + + using IMemoryOwner tileEdgeReferencesOwner = allocator.Allocate(totalReferences); + Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; + Span tileEdgeReferences = tileEdgeReferencesMemory.Span; + + for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) + { + int startTile = edgeBuffer[edgeIndex].MinRow / tileHeight; + int endTile = edgeBuffer[edgeIndex].MaxRow / tileHeight; + for (int t = startTile; t <= endTile; t++) + { + // Scatter edge indices into each tile's contiguous bucket. + tileEdgeReferences[tileWriteCursor[t]++] = edgeIndex; + } + } + + ParallelOptions parallelOptions = new() + { + MaxDegreeOfParallelism = Math.Min(MaxParallelWorkerCount, Math.Min(Environment.ProcessorCount, tileCount)) + }; + + _ = Parallel.For( + 0, + tileCount, + parallelOptions, + () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), + (tileIndex, _, worker) => + { + Context context = default; + bool hasCoverage = false; + int tile = tileIndex; + int bandTop = tile * tileHeight; + try + { + ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; + Span tileOffsets = tileOffsetsMemory.Span; + Span tileEdgeReferences = tileEdgeReferencesMemory.Span; + int bandHeight = Math.Min(tileHeight, height - bandTop); + int start = tileOffsets[tile]; + int length = tileOffsets[tile + 1] - start; + if (length > 0) + { + ReadOnlySpan tileEdges = tileEdgeReferences.Slice(start, length); + context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, tileEdges, bandTop); + hasCoverage = true; + context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); + } + } + finally + { + if (hasCoverage) + { + context.ResetTouchedRows(); + } + } + + return worker; + }, + static worker => worker.Dispose()); + + return true; + } + + /// + /// Rasterizes a single tile directly into the caller callback. + /// + /// + /// This avoids parallel setup for tiny workloads while preserving + /// the same scan-conversion math as the general tiled path. + /// + /// Prebuilt edge table. + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + private static void RasterizeSingleTileDirect( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler) + { + using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); + Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, bandTop: 0); + context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } + + /// + /// Builds an edge table in scanner-local coordinates. + /// + /// Input tessellated rings. + /// Interest left in absolute coordinates. + /// Interest top in absolute coordinates. + /// Interest height in pixels. + /// Horizontal sampling offset. + /// Vertical sampling offset. + /// Destination span for edge records. + /// Number of valid edge records written. + private static int BuildEdgeTable( + TessellatedMultipolygon multipolygon, + int minX, + int minY, + int height, + float samplingOffsetX, + float samplingOffsetY, + Span destination) + { + int count = 0; + foreach (TessellatedMultipolygon.Ring ring in multipolygon) + { + ReadOnlySpan vertices = ring.Vertices; + for (int i = 0; i < ring.VertexCount; i++) + { + PointF p0 = vertices[i]; + PointF p1 = vertices[i + 1]; + + float x0 = (p0.X - minX) + samplingOffsetX; + float y0 = (p0.Y - minY) + samplingOffsetY; + float x1 = (p1.X - minX) + samplingOffsetX; + float y1 = (p1.Y - minY) + samplingOffsetY; + + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, height)) + { + continue; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow); + destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); + } + } + + return count; + } + + /// + /// Converts bit count to the number of machine words needed to hold the bitset row. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + + /// + /// Calculates the maximum reusable band height under memory and indexing constraints. + /// + /// Interest width. + /// Interest height. + /// Bitset words per row. + /// Cover-area stride in ints. + /// Resulting maximum safe band height. + /// when a valid band height was produced. + private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) + { + bandHeight = 0; + if (width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0) + { + return false; + } + + long bytesPerRow = + ((long)wordsPerRow * nint.Size) + + (coverStride * sizeof(int)) + + sizeof(int); + + long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow; + if (rowsByBudget < 1) + { + rowsByBudget = 1; + } + + long rowsByBitVectors = int.MaxValue / wordsPerRow; + long rowsByCoverArea = int.MaxValue / coverStride; + long maxRows = Math.Min(rowsByBudget, Math.Min(rowsByBitVectors, rowsByCoverArea)); + if (maxRows < 1) + { + return false; + } + + bandHeight = (int)Math.Min(height, maxRows); + return bandHeight > 0; + } + + /// + /// Converts a float coordinate to signed 24.8 fixed-point. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + + /// + /// Computes the inclusive row range affected by a clipped non-horizontal edge. + /// + /// Edge start Y in 24.8 fixed-point. + /// Edge end Y in 24.8 fixed-point. + /// First affected integer scan row. + /// Last affected integer scan row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ComputeEdgeRowBounds(int y0, int y1, out int minRow, out int maxRow) + { + int y0Row = y0 >> FixedShift; + int y1Row = y1 >> FixedShift; + + // First touched row is floor(min(y0, y1)). + minRow = y0Row < y1Row ? y0Row : y1Row; + + int y0Fraction = y0 & (FixedOne - 1); + int y1Fraction = y1 & (FixedOne - 1); + + // Last touched row is ceil(max(y)) - 1: + // - when fractional part is non-zero, row is unchanged; + // - when exactly on a row boundary, subtract 1 (edge ownership rule). + int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1); + int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1); + maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate; + } + + /// + /// Clips a fixed-point segment against vertical bounds. + /// + /// Segment start X in 24.8 fixed-point (updated in place). + /// Segment start Y in 24.8 fixed-point (updated in place). + /// Segment end X in 24.8 fixed-point (updated in place). + /// Segment end Y in 24.8 fixed-point (updated in place). + /// Minimum Y bound in 24.8 fixed-point. + /// Maximum Y bound in 24.8 fixed-point. + /// when a non-horizontal clipped segment remains. + private static bool ClipToVerticalBoundsFixed(ref int x0, ref int y0, ref int x1, ref int y1, int minY, int maxY) + { + double t0 = 0D; + double t1 = 1D; + int originX0 = x0; + int originY0 = y0; + long dx = (long)x1 - originX0; + long dy = (long)y1 - originY0; + if (!ClipTestFixed(-(double)dy, originY0 - (double)minY, ref t0, ref t1)) + { + return false; + } + + if (!ClipTestFixed(dy, maxY - (double)originY0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1D) + { + x1 = originX0 + (int)Math.Round(dx * t1); + y1 = originY0 + (int)Math.Round(dy * t1); + } + + if (t0 > 0D) + { + x0 = originX0 + (int)Math.Round(dx * t0); + y0 = originY0 + (int)Math.Round(dy * t0); + } + + return y0 != y1; + } + + /// + /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. + /// + /// Segment start X (updated in place). + /// Segment start Y (updated in place). + /// Segment end X (updated in place). + /// Segment end Y (updated in place). + /// Minimum Y bound. + /// Maximum Y bound. + /// when a non-horizontal clipped segment remains. + private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) + { + float t0 = 0F; + float t1 = 1F; + float dx = x1 - x0; + float dy = y1 - y0; + + if (!ClipTest(-dy, y0 - minY, ref t0, ref t1)) + { + return false; + } + + if (!ClipTest(dy, maxY - y0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1F) + { + x1 = x0 + (dx * t1); + y1 = y0 + (dy * t1); + } + + if (t0 > 0F) + { + x0 += dx * t0; + y0 += dy * t0; + } + + return y0 != y1; + } + + /// + /// One Liang-Barsky clip test step. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTest(float p, float q, ref float t0, ref float t1) + { + if (p == 0F) + { + return q >= 0F; + } + + float r = q / p; + if (p < 0F) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + + /// + /// One Liang-Barsky clip test step for fixed-point clipping. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTestFixed(double p, double q, ref double t0, ref double t1) + { + if (p == 0D) + { + return q >= 0D; + } + + double r = q / p; + if (p < 0D) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + + /// + /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. + /// This is used to keep edge ownership consistent for vertical lines. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FindAdjustment(int value) + { + int lte0 = ~((value - 1) >> 31) & 1; + int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; + return lte0 & divisibleBy256; + } + + /// + /// Machine-word trailing zero count used for sparse bitset iteration. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TrailingZeroCount(nuint value) + => nint.Size == sizeof(ulong) + ? BitOperations.TrailingZeroCount((ulong)value) + : BitOperations.TrailingZeroCount((uint)value); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInterestBoundsTooLarge() + => throw new ImageProcessingException("The rasterizer interest bounds are too large for DefaultRasterizer buffers."); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowBandHeightExceedsScratchCapacity() + => throw new ImageProcessingException("Requested band height exceeds worker scratch capacity."); + + /// + /// Band/tile-local scanner context that owns mutable coverage accumulation state. + /// + /// + /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. + /// + private ref struct Context + { + private readonly Span bitVectors; + private readonly Span coverArea; + private readonly Span startCover; + private readonly Span rowMinTouchedColumn; + private readonly Span rowMaxTouchedColumn; + private readonly Span rowHasBits; + private readonly Span rowTouched; + private readonly Span touchedRows; + private readonly int width; + private readonly int height; + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly IntersectionRule intersectionRule; + private readonly RasterizationMode rasterizationMode; + private int touchedRowCount; + + /// + /// Initializes a new instance of the struct. + /// + public Context( + Span bitVectors, + Span coverArea, + Span startCover, + Span rowMinTouchedColumn, + Span rowMaxTouchedColumn, + Span rowHasBits, + Span rowTouched, + Span touchedRows, + int width, + int height, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode) + { + this.bitVectors = bitVectors; + this.coverArea = coverArea; + this.startCover = startCover; + this.rowMinTouchedColumn = rowMinTouchedColumn; + this.rowMaxTouchedColumn = rowMaxTouchedColumn; + this.rowHasBits = rowHasBits; + this.rowTouched = rowTouched; + this.touchedRows = touchedRows; + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.touchedRowCount = 0; + } + + /// + /// Rasterizes all edges in a tessellated multipolygon directly into this context. + /// + /// Input tessellated rings. + /// Absolute left coordinate of the current scanner window. + /// Absolute top coordinate of the current scanner window. + /// Horizontal sample origin offset. + /// Vertical sample origin offset. + public void RasterizeMultipolygon( + TessellatedMultipolygon multipolygon, + int minX, + int minY, + float samplingOffsetX, + float samplingOffsetY) + { + foreach (TessellatedMultipolygon.Ring ring in multipolygon) + { + ReadOnlySpan vertices = ring.Vertices; + for (int i = 0; i < ring.VertexCount; i++) + { + PointF p0 = vertices[i]; + PointF p1 = vertices[i + 1]; + + float x0 = (p0.X - minX) + samplingOffsetX; + float y0 = (p0.Y - minY) + samplingOffsetY; + float x1 = (p1.X - minX) + samplingOffsetX; + float y1 = (p1.Y - minY) + samplingOffsetY; + + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) + { + continue; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + this.RasterizeLine(fx0, fy0, fx1, fy1); + } + } + } + + /// + /// Rasterizes all prebuilt edges that overlap this context. + /// + /// Shared edge table. + /// Top row of this context in global scanner-local coordinates. + public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) + { + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); + + for (int i = 0; i < edges.Length; i++) + { + EdgeData edge = edges[i]; + int x0 = edge.X0; + int y0 = edge.Y0; + int x1 = edge.X1; + int y1 = edge.Y1; + + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) + { + continue; + } + + // Convert global scanner Y to band-local Y after clipping. + y0 -= bandTopFixed; + y1 -= bandTopFixed; + + this.RasterizeLine(x0, y0, x1, y1); + } + } + + /// + /// Rasterizes a subset of prebuilt edges that intersect this context's vertical range. + /// + /// Shared edge table. + /// Indices into for this band/tile. + /// Top row of this context in global scanner-local coordinates. + public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan edgeIndices, int bandTop) + { + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); + + for (int i = 0; i < edgeIndices.Length; i++) + { + EdgeData edge = edges[edgeIndices[i]]; + int x0 = edge.X0; + int y0 = edge.Y0; + int x1 = edge.X1; + int y1 = edge.Y1; + + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) + { + continue; + } + + // Convert global scanner Y to band-local Y after clipping. + y0 -= bandTopFixed; + y1 -= bandTopFixed; + + this.RasterizeLine(x0, y0, x1, y1); + } + } + + /// + /// Converts accumulated cover/area tables into non-zero coverage span callbacks. + /// + /// Absolute destination Y corresponding to row zero in this context. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// Coverage callback invoked for each emitted non-zero span. + public readonly void EmitCoverageRows(int destinationTop, Span scanline, RasterizerCoverageRowHandler rowHandler) + { + for (int row = 0; row < this.height; row++) + { + int rowCover = this.startCover[row]; + bool rowHasBits = this.rowHasBits[row] != 0; + if (rowCover == 0 && !rowHasBits) + { + // Nothing contributed to this row. + continue; + } + + if (!rowHasBits) + { + // No touched cells in this row, but carry cover from x < 0 can still + // produce a full-width constant span. + float coverage = this.AreaToCoverage(rowCover << AreaToCoverageShift); + if (coverage > 0F) + { + scanline[..this.width].Fill(coverage); + rowHandler(destinationTop + row, 0, scanline[..this.width]); + } + + continue; + } + + int minTouchedColumn = this.rowMinTouchedColumn[row]; + int maxTouchedColumn = this.rowMaxTouchedColumn[row]; + ReadOnlySpan rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); + this.EmitRowCoverage( + rowBitVectors, + row, + rowCover, + minTouchedColumn, + maxTouchedColumn, + destinationTop + row, + scanline, + rowHandler); + } + } + + /// + /// Clears only rows touched during the previous rasterization pass. + /// + /// + /// This sparse reset strategy avoids clearing full scratch buffers when geometry is sparse. + /// + public void ResetTouchedRows() + { + // Reset only rows that received contributions in this band. This avoids clearing + // full temporary buffers when geometry is sparse relative to the interest bounds. + for (int i = 0; i < this.touchedRowCount; i++) + { + int row = this.touchedRows[i]; + this.startCover[row] = 0; + this.rowTouched[row] = 0; + + if (this.rowHasBits[row] == 0) + { + continue; + } + + this.rowHasBits[row] = 0; + + // Clear only touched bitset words for this row. + int minWord = this.rowMinTouchedColumn[row] / WordBitCount; + int maxWord = this.rowMaxTouchedColumn[row] / WordBitCount; + int wordCount = (maxWord - minWord) + 1; + this.bitVectors.Slice((row * this.wordsPerRow) + minWord, wordCount).Clear(); + } + + this.touchedRowCount = 0; + } + + /// + /// Emits one row by iterating touched columns and coalescing equal-coverage spans. + /// + /// Bitset words indicating touched columns in this row. + /// Row index inside the context. + /// Initial carry cover value from x less than zero contributions. + /// Minimum touched column index in this row. + /// Maximum touched column index in this row. + /// Absolute destination y for this row. + /// Reusable scanline coverage buffer used for per-span materialization. + /// Coverage callback invoked for each emitted non-zero span. + private readonly void EmitRowCoverage( + ReadOnlySpan rowBitVectors, + int row, + int cover, + int minTouchedColumn, + int maxTouchedColumn, + int destinationY, + Span scanline, + RasterizerCoverageRowHandler rowHandler) + { + int rowOffset = row * this.coverStride; + int spanStart = 0; + int spanEnd = 0; + float spanCoverage = 0F; + int runStart = -1; + int runEnd = -1; + int minWord = minTouchedColumn / WordBitCount; + int maxWord = maxTouchedColumn / WordBitCount; + + for (int wordIndex = minWord; wordIndex <= maxWord; wordIndex++) + { + // Iterate touched columns sparsely by scanning set bits only. + nuint bitset = rowBitVectors[wordIndex]; + while (bitset != 0) + { + int localBitIndex = TrailingZeroCount(bitset); + bitset &= bitset - 1; + + int x = (wordIndex * WordBitCount) + localBitIndex; + if ((uint)x >= (uint)this.width) + { + continue; + } + + int tableIndex = rowOffset + (x << 1); + + // Area uses current cover before adding this cell's delta. This matches + // scan-conversion math where area integrates the edge state at cell entry. + int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); + float coverage = this.AreaToCoverage(area); + + if (spanEnd == x) + { + if (coverage <= 0F) + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x + 1; + spanEnd = spanStart; + spanCoverage = 0F; + } + else if (coverage == spanCoverage) + { + spanEnd = x + 1; + } + else + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + // We jumped over untouched columns. If cover != 0 the gap has a constant + // non-zero coverage and must be emitted as its own run. + if (cover == 0) + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else + { + float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); + if (gapCoverage <= 0F) + { + // Even-odd can map non-zero winding to zero coverage. + // Treat this as a hard run break so we don't bridge holes. + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else if (spanCoverage == gapCoverage) + { + if (coverage == gapCoverage) + { + spanEnd = x + 1; + } + else + { + WriteSpan(scanline, spanStart, x, spanCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + WriteSpan(scanline, spanEnd, x, gapCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + } + + cover += this.coverArea[tableIndex]; + } + } + + // Flush tail run and any remaining constant-cover tail after the last touched cell. + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + if (cover != 0 && spanEnd < this.width) + { + WriteSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift), ref runStart, ref runEnd); + } + + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + } + + /// + /// Converts accumulated signed area to normalized coverage under the selected fill rule. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly float AreaToCoverage(int area) + { + int signedArea = area >> AreaToCoverageShift; + int absoluteArea = signedArea < 0 ? -signedArea : signedArea; + float coverage; + if (this.intersectionRule == IntersectionRule.NonZero) + { + // Non-zero winding clamps absolute winding accumulation to [0, 1]. + if (absoluteArea >= CoverageStepCount) + { + coverage = 1F; + } + else + { + coverage = absoluteArea * CoverageScale; + } + } + else + { + // Even-odd wraps every 2*CoverageStepCount and mirrors second half. + int wrapped = absoluteArea & EvenOddMask; + if (wrapped > CoverageStepCount) + { + wrapped = EvenOddPeriod - wrapped; + } + + coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; + } + + if (this.rasterizationMode == RasterizationMode.Aliased) + { + // Aliased mode quantizes final coverage to hard 0/1 per pixel. + return coverage >= 0.5F ? 1F : 0F; + } + + return coverage; + } + + /// + /// Writes one non-zero coverage segment into the scanline and expands the active run. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteSpan( + Span scanline, + int start, + int end, + float coverage, + ref int runStart, + ref int runEnd) + { + if (coverage <= 0F || end <= start) + { + return; + } + + scanline[start..end].Fill(coverage); + if (runStart < 0) + { + runStart = start; + runEnd = end; + return; + } + + if (end > runEnd) + { + runEnd = end; + } + } + + /// + /// Emits the currently accumulated non-zero run, if any. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EmitRun( + RasterizerCoverageRowHandler rowHandler, + int destinationY, + Span scanline, + ref int runStart, + ref int runEnd) + { + if (runStart < 0) + { + return; + } + + rowHandler(destinationY, runStart, scanline[runStart..runEnd]); + runStart = -1; + runEnd = -1; + } + + /// + /// Sets a row/column bit and reports whether it was newly set. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly bool ConditionalSetBit(int row, int column) + { + int bitIndex = row * this.wordsPerRow; + int wordIndex = bitIndex + (column / WordBitCount); + nuint mask = (nuint)1 << (column % WordBitCount); + ref nuint word = ref this.bitVectors[wordIndex]; + bool newlySet = (word & mask) == 0; + word |= mask; + + // Fast row-level early-out for EmitCoverageRows. + this.rowHasBits[row] = 1; + return newlySet; + } + + /// + /// Adds one cell contribution into cover/area accumulators. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddCell(int row, int column, int delta, int area) + { + if ((uint)row >= (uint)this.height) + { + return; + } + + this.MarkRowTouched(row); + + if (column < 0) + { + // Contributions left of x=0 accumulate into the row carry. + this.startCover[row] += delta; + return; + } + + if ((uint)column >= (uint)this.width) + { + return; + } + + int index = (row * this.coverStride) + (column << 1); + bool rowHadBits = this.rowHasBits[row] != 0; + if (this.ConditionalSetBit(row, column)) + { + // First write wins initialization path avoids reading old values. + this.coverArea[index] = delta; + this.coverArea[index + 1] = area; + } + else + { + // Multiple edges can hit the same cell; accumulate signed values. + this.coverArea[index] += delta; + this.coverArea[index + 1] += area; + } + + if (!rowHadBits) + { + this.rowMinTouchedColumn[row] = column; + this.rowMaxTouchedColumn[row] = column; + } + else + { + if (column < this.rowMinTouchedColumn[row]) + { + this.rowMinTouchedColumn[row] = column; + } + + if (column > this.rowMaxTouchedColumn[row]) + { + this.rowMaxTouchedColumn[row] = column; + } + } + } + + /// + /// Marks a row as touched once so sparse reset can clear it later. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MarkRowTouched(int row) + { + if (this.rowTouched[row] != 0) + { + return; + } + + this.rowTouched[row] = 1; + this.touchedRows[this.touchedRowCount++] = row; + } + + /// + /// Emits one vertical cell contribution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CellVertical(int px, int py, int x, int y0, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x - x); + this.AddCell(py, px, delta, area); + } + + /// + /// Emits one general cell contribution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Cell(int row, int px, int x0, int y0, int x1, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x0 - x1); + this.AddCell(row, px, delta, area); + } + + /// + /// Rasterizes a downward vertical edge segment. + /// + private void VerticalDown(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + // Entire segment stays within one row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + // First partial row, full middle rows, last partial row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); + for (int row = rowIndex0 + 1; row < rowIndex1; row++) + { + this.CellVertical(columnIndex, row, fx, 0, FixedOne); + } + + this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); + } + + /// + /// Rasterizes an upward vertical edge segment. + /// + private void VerticalUp(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = (y0 - 1) >> FixedShift; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + // Entire segment stays within one row. + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + // First partial row, full middle rows, last partial row (upward direction). + this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); + for (int row = rowIndex0 - 1; row > rowIndex1; row--) + { + this.CellVertical(columnIndex, row, fx, FixedOne, 0); + } + + this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); + } + + // The following row/line helpers are directional variants of the same fixed-point edge + // walker. They are intentionally split to minimize branch costs in hot loops. + + /// + /// Rasterizes a downward, left-to-right segment within a single row. + /// + private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p1y - p0y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + /// + /// RowDownR variant that handles perfectly vertical edge ownership consistently. + /// + private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes an upward, left-to-right segment within a single row. + /// + private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p0y - p1y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + /// + /// RowUpR variant that handles perfectly vertical edge ownership consistently. + /// + private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes a downward, right-to-left segment within a single row. + /// + private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p1y - p0y; + int pp = fx0 * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + /// + /// RowDownL variant that handles perfectly vertical edge ownership consistently. + /// + private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes an upward, right-to-left segment within a single row. + /// + private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p0y - p1y; + int pp = fx0 * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + /// + /// RowUpL variant that handles perfectly vertical edge ownership consistently. + /// + private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + /// + /// Rasterizes a downward, left-to-right segment spanning multiple rows. + /// + private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // p/delta/mod/rem implement an integer DDA that advances x at row boundaries + // without per-row floating-point math. + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowDownR_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); + } + + /// + /// Rasterizes an upward, left-to-right segment spanning multiple rows. + /// + private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward version of the same integer DDA stepping as LineDownR. + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowUpR_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + /// + /// Rasterizes a downward, right-to-left segment spanning multiple rows. + /// + private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Right-to-left variant of the integer DDA. + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowDownL_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); + } + + /// + /// Rasterizes an upward, right-to-left segment spanning multiple rows. + /// + private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward + right-to-left variant of the integer DDA. + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowUpL_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + /// + /// Dispatches a clipped edge to the correct directional fixed-point walker. + /// + private void RasterizeLine(int x0, int y0, int x1, int y1) + { + if (x0 == x1) + { + // Vertical edges need ownership adjustment to avoid double counting at cell seams. + int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; + if (y0 < y1) + { + this.VerticalDown(columnIndex, y0, y1, x0); + } + else + { + this.VerticalUp(columnIndex, y0, y1, x0); + } + + return; + } + + if (y0 < y1) + { + // Downward edges use inclusive top/exclusive bottom row mapping. + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + if (rowIndex0 == rowIndex1) + { + int rowBase = rowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowDownR(rowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowDownL(rowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + else + { + this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + + return; + } + + // Upward edges mirror the mapping to preserve winding consistency. + int upRowIndex0 = (y0 - 1) >> FixedShift; + int upRowIndex1 = y1 >> FixedShift; + if (upRowIndex0 == upRowIndex1) + { + int rowBase = upRowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + else + { + this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + } + } + + /// + /// Immutable scanner-local edge record with precomputed affected-row bounds. + /// + /// + /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path + /// access without per-read unpacking. + /// + private readonly struct EdgeData + { + /// + /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X0; + + /// + /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y0; + + /// + /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X1; + + /// + /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y1; + + /// + /// Gets the first scanner row affected by this edge. + /// + public readonly int MinRow; + + /// + /// Gets the last scanner row affected by this edge. + /// + public readonly int MaxRow; + + /// + /// Initializes a new instance of the struct. + /// + public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.MinRow = minRow; + this.MaxRow = maxRow; + } + } + + /// + /// Reusable per-worker scratch buffers used by tiled and sequential band rasterization. + /// + private sealed class WorkerScratch : IDisposable + { + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly int width; + private readonly int tileCapacity; + private readonly IMemoryOwner bitVectorsOwner; + private readonly IMemoryOwner coverAreaOwner; + private readonly IMemoryOwner startCoverOwner; + private readonly IMemoryOwner rowMinTouchedColumnOwner; + private readonly IMemoryOwner rowMaxTouchedColumnOwner; + private readonly IMemoryOwner rowHasBitsOwner; + private readonly IMemoryOwner rowTouchedOwner; + private readonly IMemoryOwner touchedRowsOwner; + private readonly IMemoryOwner scanlineOwner; + + private WorkerScratch( + int wordsPerRow, + int coverStride, + int width, + int tileCapacity, + IMemoryOwner bitVectorsOwner, + IMemoryOwner coverAreaOwner, + IMemoryOwner startCoverOwner, + IMemoryOwner rowMinTouchedColumnOwner, + IMemoryOwner rowMaxTouchedColumnOwner, + IMemoryOwner rowHasBitsOwner, + IMemoryOwner rowTouchedOwner, + IMemoryOwner touchedRowsOwner, + IMemoryOwner scanlineOwner) + { + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.width = width; + this.tileCapacity = tileCapacity; + this.bitVectorsOwner = bitVectorsOwner; + this.coverAreaOwner = coverAreaOwner; + this.startCoverOwner = startCoverOwner; + this.rowMinTouchedColumnOwner = rowMinTouchedColumnOwner; + this.rowMaxTouchedColumnOwner = rowMaxTouchedColumnOwner; + this.rowHasBitsOwner = rowHasBitsOwner; + this.rowTouchedOwner = rowTouchedOwner; + this.touchedRowsOwner = touchedRowsOwner; + this.scanlineOwner = scanlineOwner; + } + + /// + /// Gets reusable scanline scratch for this worker. + /// + public Span Scanline => this.scanlineOwner.Memory.Span; + + /// + /// Allocates worker-local scratch sized for the configured tile/band capacity. + /// + public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, int coverStride, int width, int tileCapacity) + { + int bitVectorCapacity = checked(wordsPerRow * tileCapacity); + int coverAreaCapacity = checked(coverStride * tileCapacity); + IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); + IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); + IMemoryOwner startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowMinTouchedColumnOwner = allocator.Allocate(tileCapacity); + IMemoryOwner rowMaxTouchedColumnOwner = allocator.Allocate(tileCapacity); + IMemoryOwner rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner touchedRowsOwner = allocator.Allocate(tileCapacity); + IMemoryOwner scanlineOwner = allocator.Allocate(width); + + return new WorkerScratch( + wordsPerRow, + coverStride, + width, + tileCapacity, + bitVectorsOwner, + coverAreaOwner, + startCoverOwner, + rowMinTouchedColumnOwner, + rowMaxTouchedColumnOwner, + rowHasBitsOwner, + rowTouchedOwner, + touchedRowsOwner, + scanlineOwner); + } + + /// + /// Creates a context view over this scratch for the requested band height. + /// + public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode) + { + if ((uint)bandHeight > (uint)this.tileCapacity) + { + ThrowBandHeightExceedsScratchCapacity(); + } + + int bitVectorCount = checked(this.wordsPerRow * bandHeight); + int coverAreaCount = checked(this.coverStride * bandHeight); + return new Context( + this.bitVectorsOwner.Memory.Span[..bitVectorCount], + this.coverAreaOwner.Memory.Span[..coverAreaCount], + this.startCoverOwner.Memory.Span[..bandHeight], + this.rowMinTouchedColumnOwner.Memory.Span[..bandHeight], + this.rowMaxTouchedColumnOwner.Memory.Span[..bandHeight], + this.rowHasBitsOwner.Memory.Span[..bandHeight], + this.rowTouchedOwner.Memory.Span[..bandHeight], + this.touchedRowsOwner.Memory.Span[..bandHeight], + this.width, + bandHeight, + this.wordsPerRow, + this.coverStride, + intersectionRule, + rasterizationMode); + } + + /// + /// Releases worker-local scratch buffers back to the allocator. + /// + public void Dispose() + { + this.bitVectorsOwner.Dispose(); + this.coverAreaOwner.Dispose(); + this.startCoverOwner.Dispose(); + this.rowMinTouchedColumnOwner.Dispose(); + this.rowMaxTouchedColumnOwner.Dispose(); + this.rowHasBitsOwner.Dispose(); + this.rowTouchedOwner.Dispose(); + this.touchedRowsOwner.Dispose(); + this.scanlineOwner.Dispose(); + } } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs deleted file mode 100644 index 480d7a636..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ /dev/null @@ -1,2292 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Fixed-point polygon scanner that converts polygon edges into per-row coverage runs. -/// -/// -/// The scanner has two execution modes: -/// 1. Parallel tiled execution (default): build an edge table once, bucket edges by tile rows, -/// rasterize tiles in parallel with worker-local scratch, then emit in deterministic Y order. -/// 2. Sequential execution: reuse the same edge table and process band buckets on one thread. -/// -/// Both modes share the same coverage math and fill-rule handling, ensuring predictable output -/// regardless of scheduling strategy. -/// -internal static class PolygonScanner -{ - // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). - // Keeping this bounded prevents pathological full-image allocations on very large interests. - private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; - - // Blaze-style tile height used by the parallel row-tiling pipeline. - private const int DefaultTileHeight = 16; - - // Cap for buffered output coverage in the parallel path. We buffer one float per destination - // pixel plus one dirty-row byte per tile row before deterministic ordered emission. - private const long ParallelOutputPixelBudget = 16L * 1024L * 1024L; // 4096 x 4096 - - private const int FixedShift = 8; - private const int FixedOne = 1 << FixedShift; - private static readonly int WordBitCount = nint.Size * 8; - private const int AreaToCoverageShift = 9; - private const int CoverageStepCount = 256; - private const int EvenOddMask = (CoverageStepCount * 2) - 1; - private const int EvenOddPeriod = CoverageStepCount * 2; - private const float CoverageScale = 1F / CoverageStepCount; - - /// - /// Rasterizes the path using the default execution policy. - /// - /// The caller-owned mutable state type. - /// Path to rasterize. - /// Rasterization options. - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - public static void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - => RasterizeCore(path, options, allocator, ref state, scanlineHandler, allowParallel: true); - - /// - /// Rasterizes the path using the forced sequential policy. - /// - /// The caller-owned mutable state type. - /// Path to rasterize. - /// Rasterization options. - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - public static void RasterizeSequential( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - => RasterizeCore(path, options, allocator, ref state, scanlineHandler, allowParallel: false); - - /// - /// Shared entry point used by both public execution policies. - /// - /// The caller-owned mutable state type. - /// Path to rasterize. - /// Rasterization options. - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - /// - /// If , the scanner may use parallel tiled execution when profitable. - /// - private static void RasterizeCore( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler, - bool allowParallel) - where TState : struct - { - Rectangle interest = options.Interest; - int width = interest.Width; - int height = interest.Height; - if (width <= 0 || height <= 0) - { - return; - } - - int wordsPerRow = BitVectorsForMaxBitCount(width); - int maxBandRows = 0; - long coverStride = (long)width * 2; - if (coverStride > int.MaxValue || - !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) - { - ThrowInterestBoundsTooLarge(); - } - - int coverStrideInt = (int)coverStride; - bool samplePixelCenter = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter; - float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; - float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; - - // Create tessellated rings once. Both sequential and parallel paths consume this single - // canonical representation so path flattening/orientation work is never repeated. - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); - using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); - int edgeCount = BuildEdgeTable( - multipolygon, - interest.Left, - interest.Top, - height, - samplingOffsetX, - samplingOffsetY, - edgeDataOwner.Memory.Span); - if (edgeCount <= 0) - { - return; - } - - if (allowParallel && - TryRasterizeParallel( - edgeDataOwner.Memory, - edgeCount, - width, - height, - interest.Top, - wordsPerRow, - coverStrideInt, - maxBandRows, - options.IntersectionRule, - options.RasterizationMode, - allocator, - ref state, - scanlineHandler)) - { - return; - } - - RasterizeSequentialBands( - edgeDataOwner.Memory.Span[..edgeCount], - width, - height, - interest.Top, - wordsPerRow, - coverStrideInt, - maxBandRows, - options.IntersectionRule, - options.RasterizationMode, - allocator, - ref state, - scanlineHandler); - } - - /// - /// Sequential implementation using band buckets over the prebuilt edge table. - /// - /// The caller-owned mutable state type. - /// Prebuilt edges in scanner-local coordinates. - /// Destination width in pixels. - /// Destination height in pixels. - /// Absolute top Y of the interest rectangle. - /// Bit-vector words per row. - /// Cover-area stride in ints. - /// Maximum rows per reusable scratch band. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void RasterizeSequentialBands( - ReadOnlySpan edges, - int width, - int height, - int interestTop, - int wordsPerRow, - int coverStrideInt, - int maxBandRows, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - int bandHeight = maxBandRows; - int bandCount = (height + bandHeight - 1) / bandHeight; - if (bandCount < 1) - { - return; - } - - using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); - Span bandCounts = bandCountsOwner.Memory.Span; - long totalBandEdgeReferences = 0; - for (int i = 0; i < edges.Length; i++) - { - // Each edge can overlap multiple bands. We first count references so we can build - // a compact contiguous index list (CSR-style) without per-band allocations. - int startBand = edges[i].MinRow / bandHeight; - int endBand = edges[i].MaxRow / bandHeight; - totalBandEdgeReferences += (endBand - startBand) + 1; - if (totalBandEdgeReferences > int.MaxValue) - { - ThrowInterestBoundsTooLarge(); - } - - for (int b = startBand; b <= endBand; b++) - { - bandCounts[b]++; - } - } - - int totalReferences = (int)totalBandEdgeReferences; - using IMemoryOwner bandOffsetsOwner = allocator.Allocate(bandCount + 1); - Span bandOffsets = bandOffsetsOwner.Memory.Span; - int offset = 0; - for (int b = 0; b < bandCount; b++) - { - // Prefix sum: bandOffsets[b] is the start index of band b inside bandEdgeReferences. - bandOffsets[b] = offset; - offset += bandCounts[b]; - } - - bandOffsets[bandCount] = offset; - using IMemoryOwner bandWriteCursorOwner = allocator.Allocate(bandCount); - Span bandWriteCursor = bandWriteCursorOwner.Memory.Span; - bandOffsets[..bandCount].CopyTo(bandWriteCursor); - - using IMemoryOwner bandEdgeReferencesOwner = allocator.Allocate(totalReferences); - Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; - for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) - { - // Scatter each edge index to all bands touched by its row range. - int startBand = edges[edgeIndex].MinRow / bandHeight; - int endBand = edges[edgeIndex].MaxRow / bandHeight; - for (int b = startBand; b <= endBand; b++) - { - bandEdgeReferences[bandWriteCursor[b]++] = edgeIndex; - } - } - - using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); - for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) - { - int bandTop = bandIndex * bandHeight; - int currentBandHeight = Math.Min(bandHeight, height - bandTop); - int start = bandOffsets[bandIndex]; - int length = bandOffsets[bandIndex + 1] - start; - if (length == 0) - { - // No edge crosses this band, so there is nothing to rasterize or clear. - continue; - } - - Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); - ReadOnlySpan bandEdges = bandEdgeReferences.Slice(start, length); - context.RasterizeEdgeTable(edges, bandEdges, bandTop); - context.EmitScanlines(interestTop + bandTop, scratch.Scanline, ref state, scanlineHandler); - context.ResetTouchedRows(); - } - } - - /// - /// Attempts to execute the tiled parallel scanner. - /// - /// The caller-owned mutable state type. - /// Memory block containing prebuilt edges. - /// Number of valid edges in . - /// Destination width in pixels. - /// Destination height in pixels. - /// Absolute top Y of the interest rectangle. - /// Bit-vector words per row. - /// Cover-area stride in ints. - /// Maximum rows per worker scratch context. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - /// - /// when the tiled path executed successfully; - /// when the caller should run sequential fallback. - /// - private static bool TryRasterizeParallel( - Memory edgeMemory, - int edgeCount, - int width, - int height, - int interestTop, - int wordsPerRow, - int coverStride, - int maxBandRows, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); - if (tileHeight < 1) - { - return false; - } - - int tileCount = (height + tileHeight - 1) / tileHeight; - if (tileCount == 1) - { - // Tiny workload fast path: avoid bucket construction, worker scheduling, and - // tile-output buffering when everything fits in a single tile. - RasterizeSingleTileDirect( - edgeMemory.Span[..edgeCount], - width, - height, - interestTop, - wordsPerRow, - coverStride, - intersectionRule, - rasterizationMode, - allocator, - ref state, - scanlineHandler); - return true; - } - - if (Environment.ProcessorCount < 2) - { - return false; - } - - long totalPixels = (long)width * height; - if (totalPixels > ParallelOutputPixelBudget) - { - // Parallel mode buffers tile coverage before ordered emission. Skip when the - // buffered output footprint would exceed our safety budget. - return false; - } - - using IMemoryOwner tileCountsOwner = allocator.Allocate(tileCount, AllocationOptions.Clean); - Span tileCounts = tileCountsOwner.Memory.Span; - - long totalTileEdgeReferences = 0; - Span edgeBuffer = edgeMemory.Span; - for (int i = 0; i < edgeCount; i++) - { - // Same CSR construction as sequential mode, now keyed by tile instead of band. - int startTile = edgeBuffer[i].MinRow / tileHeight; - int endTile = edgeBuffer[i].MaxRow / tileHeight; - int tileSpan = (endTile - startTile) + 1; - totalTileEdgeReferences += tileSpan; - - if (totalTileEdgeReferences > int.MaxValue) - { - return false; - } - - for (int t = startTile; t <= endTile; t++) - { - tileCounts[t]++; - } - } - - int totalReferences = (int)totalTileEdgeReferences; - using IMemoryOwner tileOffsetsOwner = allocator.Allocate(tileCount + 1); - Memory tileOffsetsMemory = tileOffsetsOwner.Memory; - Span tileOffsets = tileOffsetsMemory.Span; - - int offset = 0; - for (int t = 0; t < tileCount; t++) - { - // Prefix sum over tile counts so each tile gets one contiguous slice. - tileOffsets[t] = offset; - offset += tileCounts[t]; - } - - tileOffsets[tileCount] = offset; - using IMemoryOwner tileWriteCursorOwner = allocator.Allocate(tileCount); - Span tileWriteCursor = tileWriteCursorOwner.Memory.Span; - tileOffsets[..tileCount].CopyTo(tileWriteCursor); - - using IMemoryOwner tileEdgeReferencesOwner = allocator.Allocate(totalReferences); - Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - - for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) - { - int startTile = edgeBuffer[edgeIndex].MinRow / tileHeight; - int endTile = edgeBuffer[edgeIndex].MaxRow / tileHeight; - for (int t = startTile; t <= endTile; t++) - { - // Scatter edge indices into each tile's contiguous bucket. - tileEdgeReferences[tileWriteCursor[t]++] = edgeIndex; - } - } - - TileOutput[] tileOutputs = new TileOutput[tileCount]; - ParallelOptions parallelOptions = new() - { - MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, tileCount) - }; - - try - { - _ = Parallel.For( - 0, - tileCount, - parallelOptions, - () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), - (tileIndex, _, scratch) => - { - ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; - Span tileOffsets = tileOffsetsMemory.Span; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - int bandTop = tileIndex * tileHeight; - int bandHeight = Math.Min(tileHeight, height - bandTop); - int start = tileOffsets[tileIndex]; - int length = tileOffsets[tileIndex + 1] - start; - ReadOnlySpan tileEdges = tileEdgeReferences.Slice(start, length); - - // Each tile rasterizes fully independently into worker-local scratch. - RasterizeTile( - scratch, - edges, - tileEdges, - bandTop, - bandHeight, - width, - intersectionRule, - rasterizationMode, - allocator, - tileOutputs, - tileIndex); - - return scratch; - }, - static scratch => scratch.Dispose()); - - EmitTileOutputs(tileOutputs, width, interestTop, ref state, scanlineHandler); - return true; - } - finally - { - foreach (TileOutput output in tileOutputs) - { - output?.Dispose(); - } - } - } - - /// - /// Rasterizes a single tile directly into the caller callback. - /// - /// - /// This avoids parallel setup and tile-output buffering for tiny workloads while preserving - /// the same scan-conversion math and callback ordering as the general tiled path. - /// - /// The caller-owned mutable state type. - /// Prebuilt edge table. - /// Destination width in pixels. - /// Destination height in pixels. - /// Absolute top Y of the interest rectangle. - /// Bit-vector words per row. - /// Cover-area stride in ints. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void RasterizeSingleTileDirect( - ReadOnlySpan edges, - int width, - int height, - int interestTop, - int wordsPerRow, - int coverStride, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); - Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, bandTop: 0); - context.EmitScanlines(interestTop, scratch.Scanline, ref state, scanlineHandler); - context.ResetTouchedRows(); - } - - /// - /// Rasterizes one tile/band edge subset into temporary coverage buffers. - /// - /// Worker-local scratch buffers. - /// Shared edge table. - /// Indices of edges intersecting this tile. - /// Tile top row in scanner-local coordinates. - /// Tile height in rows. - /// Destination width in pixels. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Output slot array indexed by tile ID. - /// Current tile index. - private static void RasterizeTile( - WorkerScratch scratch, - ReadOnlySpan edges, - ReadOnlySpan tileEdgeIndices, - int bandTop, - int bandHeight, - int width, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - TileOutput[] outputs, - int tileIndex) - { - if (tileEdgeIndices.Length == 0) - { - return; - } - - Context context = scratch.CreateContext(bandHeight, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, tileEdgeIndices, bandTop); - - int coverageLength = checked(width * bandHeight); - IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); - IMemoryOwner dirtyRowsOwner = allocator.Allocate(bandHeight, AllocationOptions.Clean); - bool committed = false; - - try - { - TileCaptureState captureState = new(width, coverageOwner.Memory, dirtyRowsOwner.Memory); - - // Emit with destinationTop=0 into tile-local storage; global Y is restored later. - context.EmitScanlines(0, scratch.Scanline, ref captureState, CaptureTileScanline); - outputs[tileIndex] = new TileOutput(bandTop, bandHeight, coverageOwner, dirtyRowsOwner); - committed = true; - } - finally - { - context.ResetTouchedRows(); - - if (!committed) - { - coverageOwner.Dispose(); - dirtyRowsOwner.Dispose(); - } - } - } - - /// - /// Emits buffered tile outputs in deterministic top-to-bottom order. - /// - /// The caller-owned mutable state type. - /// Tile outputs captured by workers. - /// Destination width in pixels. - /// Absolute top Y of the interest rectangle. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void EmitTileOutputs( - TileOutput[] outputs, - int width, - int destinationTop, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - foreach (TileOutput output in outputs) - { - if (output is null) - { - continue; - } - - Span coverage = output.CoverageOwner.Memory.Span; - Span dirtyRows = output.DirtyRowsOwner.Memory.Span; - for (int row = 0; row < output.Height; row++) - { - if (dirtyRows[row] == 0) - { - // Rows are sparse; untouched rows were never emitted by the tile worker. - continue; - } - - // Stable top-to-bottom emission keeps observable callback order deterministic. - Span scanline = coverage.Slice(row * width, width); - scanlineHandler(destinationTop + output.Top + row, scanline, ref state); - } - } - } - - /// - /// Captures one emitted scanline into a tile-local output buffer. - /// - /// Row index relative to tile-local coordinates. - /// Coverage scanline. - /// Tile capture state. - private static void CaptureTileScanline(int y, Span scanline, ref TileCaptureState state) - { - // y is tile-local (destinationTop was 0 in RasterizeTile). - int row = y - state.Top; - scanline.CopyTo(state.Coverage.Span.Slice(row * state.Width, state.Width)); - state.DirtyRows.Span[row] = 1; - } - - /// - /// Builds an edge table in scanner-local coordinates. - /// - /// Input tessellated rings. - /// Interest left in absolute coordinates. - /// Interest top in absolute coordinates. - /// Interest height in pixels. - /// Horizontal sampling offset. - /// Vertical sampling offset. - /// Destination span for edge records. - /// Number of valid edge records written. - private static int BuildEdgeTable( - TessellatedMultipolygon multipolygon, - int minX, - int minY, - int height, - float samplingOffsetX, - float samplingOffsetY, - Span destination) - { - int count = 0; - foreach (TessellatedMultipolygon.Ring ring in multipolygon) - { - ReadOnlySpan vertices = ring.Vertices; - for (int i = 0; i < ring.VertexCount; i++) - { - PointF p0 = vertices[i]; - PointF p1 = vertices[i + 1]; - - float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = (p0.Y - minY) + samplingOffsetY; - float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = (p1.Y - minY) + samplingOffsetY; - - if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) - { - continue; - } - - if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, height)) - { - continue; - } - - int fx0 = FloatToFixed24Dot8(x0); - int fy0 = FloatToFixed24Dot8(y0); - int fx1 = FloatToFixed24Dot8(x1); - int fy1 = FloatToFixed24Dot8(y1); - if (fy0 == fy1) - { - continue; - } - - ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow); - destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); - } - } - - return count; - } - - /// - /// Converts bit count to the number of machine words needed to hold the bitset row. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; - - /// - /// Calculates the maximum reusable band height under memory and indexing constraints. - /// - /// Interest width. - /// Interest height. - /// Bitset words per row. - /// Cover-area stride in ints. - /// Resulting maximum safe band height. - /// when a valid band height was produced. - private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) - { - bandHeight = 0; - if (width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0) - { - return false; - } - - long bytesPerRow = - ((long)wordsPerRow * nint.Size) + - (coverStride * sizeof(int)) + - sizeof(int); - - long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow; - if (rowsByBudget < 1) - { - rowsByBudget = 1; - } - - long rowsByBitVectors = int.MaxValue / wordsPerRow; - long rowsByCoverArea = int.MaxValue / coverStride; - long maxRows = Math.Min(rowsByBudget, Math.Min(rowsByBitVectors, rowsByCoverArea)); - if (maxRows < 1) - { - return false; - } - - bandHeight = (int)Math.Min(height, maxRows); - return bandHeight > 0; - } - - /// - /// Converts a float coordinate to signed 24.8 fixed-point. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); - - /// - /// Computes the inclusive row range affected by a clipped non-horizontal edge. - /// - /// Edge start Y in 24.8 fixed-point. - /// Edge end Y in 24.8 fixed-point. - /// First affected integer scan row. - /// Last affected integer scan row. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ComputeEdgeRowBounds(int y0, int y1, out int minRow, out int maxRow) - { - int y0Row = y0 >> FixedShift; - int y1Row = y1 >> FixedShift; - - // First touched row is floor(min(y0, y1)). - minRow = y0Row < y1Row ? y0Row : y1Row; - - int y0Fraction = y0 & (FixedOne - 1); - int y1Fraction = y1 & (FixedOne - 1); - - // Last touched row is ceil(max(y)) - 1: - // - when fractional part is non-zero, row is unchanged; - // - when exactly on a row boundary, subtract 1 (edge ownership rule). - int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1); - int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1); - maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate; - } - - /// - /// Clips a fixed-point segment against vertical bounds. - /// - /// Segment start X in 24.8 fixed-point (updated in place). - /// Segment start Y in 24.8 fixed-point (updated in place). - /// Segment end X in 24.8 fixed-point (updated in place). - /// Segment end Y in 24.8 fixed-point (updated in place). - /// Minimum Y bound in 24.8 fixed-point. - /// Maximum Y bound in 24.8 fixed-point. - /// when a non-horizontal clipped segment remains. - private static bool ClipToVerticalBoundsFixed(ref int x0, ref int y0, ref int x1, ref int y1, int minY, int maxY) - { - double t0 = 0D; - double t1 = 1D; - int originX0 = x0; - int originY0 = y0; - long dx = (long)x1 - originX0; - long dy = (long)y1 - originY0; - if (!ClipTestFixed(-(double)dy, originY0 - (double)minY, ref t0, ref t1)) - { - return false; - } - - if (!ClipTestFixed(dy, maxY - (double)originY0, ref t0, ref t1)) - { - return false; - } - - if (t1 < 1D) - { - x1 = originX0 + (int)Math.Round(dx * t1); - y1 = originY0 + (int)Math.Round(dy * t1); - } - - if (t0 > 0D) - { - x0 = originX0 + (int)Math.Round(dx * t0); - y0 = originY0 + (int)Math.Round(dy * t0); - } - - return y0 != y1; - } - - /// - /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. - /// - /// Segment start X (updated in place). - /// Segment start Y (updated in place). - /// Segment end X (updated in place). - /// Segment end Y (updated in place). - /// Minimum Y bound. - /// Maximum Y bound. - /// when a non-horizontal clipped segment remains. - private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) - { - float t0 = 0F; - float t1 = 1F; - float dx = x1 - x0; - float dy = y1 - y0; - - if (!ClipTest(-dy, y0 - minY, ref t0, ref t1)) - { - return false; - } - - if (!ClipTest(dy, maxY - y0, ref t0, ref t1)) - { - return false; - } - - if (t1 < 1F) - { - x1 = x0 + (dx * t1); - y1 = y0 + (dy * t1); - } - - if (t0 > 0F) - { - x0 += dx * t0; - y0 += dy * t0; - } - - return y0 != y1; - } - - /// - /// One Liang-Barsky clip test step. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ClipTest(float p, float q, ref float t0, ref float t1) - { - if (p == 0F) - { - return q >= 0F; - } - - float r = q / p; - if (p < 0F) - { - if (r > t1) - { - return false; - } - - if (r > t0) - { - t0 = r; - } - } - else - { - if (r < t0) - { - return false; - } - - if (r < t1) - { - t1 = r; - } - } - - return true; - } - - /// - /// One Liang-Barsky clip test step for fixed-point clipping. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ClipTestFixed(double p, double q, ref double t0, ref double t1) - { - if (p == 0D) - { - return q >= 0D; - } - - double r = q / p; - if (p < 0D) - { - if (r > t1) - { - return false; - } - - if (r > t0) - { - t0 = r; - } - } - else - { - if (r < t0) - { - return false; - } - - if (r < t1) - { - t1 = r; - } - } - - return true; - } - - /// - /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. - /// This is used to keep edge ownership consistent for vertical lines. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FindAdjustment(int value) - { - int lte0 = ~((value - 1) >> 31) & 1; - int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; - return lte0 & divisibleBy256; - } - - /// - /// Machine-word trailing zero count used for sparse bitset iteration. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TrailingZeroCount(nuint value) - => nint.Size == sizeof(ulong) - ? BitOperations.TrailingZeroCount((ulong)value) - : BitOperations.TrailingZeroCount((uint)value); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInterestBoundsTooLarge() - => throw new ImageProcessingException("The rasterizer interest bounds are too large for PolygonScanner buffers."); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowBandHeightExceedsScratchCapacity() - => throw new ImageProcessingException("Requested band height exceeds worker scratch capacity."); - - /// - /// Band/tile-local scanner context that owns mutable coverage accumulation state. - /// - /// - /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. - /// - private ref struct Context - { - private readonly Span bitVectors; - private readonly Span coverArea; - private readonly Span startCover; - private readonly Span rowHasBits; - private readonly Span rowTouched; - private readonly Span touchedRows; - private readonly int width; - private readonly int height; - private readonly int wordsPerRow; - private readonly int coverStride; - private readonly IntersectionRule intersectionRule; - private readonly RasterizationMode rasterizationMode; - private int touchedRowCount; - - /// - /// Initializes a new instance of the struct. - /// - public Context( - Span bitVectors, - Span coverArea, - Span startCover, - Span rowHasBits, - Span rowTouched, - Span touchedRows, - int width, - int height, - int wordsPerRow, - int coverStride, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode) - { - this.bitVectors = bitVectors; - this.coverArea = coverArea; - this.startCover = startCover; - this.rowHasBits = rowHasBits; - this.rowTouched = rowTouched; - this.touchedRows = touchedRows; - this.width = width; - this.height = height; - this.wordsPerRow = wordsPerRow; - this.coverStride = coverStride; - this.intersectionRule = intersectionRule; - this.rasterizationMode = rasterizationMode; - this.touchedRowCount = 0; - } - - /// - /// Rasterizes all edges in a tessellated multipolygon directly into this context. - /// - /// Input tessellated rings. - /// Absolute left coordinate of the current scanner window. - /// Absolute top coordinate of the current scanner window. - /// Horizontal sample origin offset. - /// Vertical sample origin offset. - public void RasterizeMultipolygon( - TessellatedMultipolygon multipolygon, - int minX, - int minY, - float samplingOffsetX, - float samplingOffsetY) - { - foreach (TessellatedMultipolygon.Ring ring in multipolygon) - { - ReadOnlySpan vertices = ring.Vertices; - for (int i = 0; i < ring.VertexCount; i++) - { - PointF p0 = vertices[i]; - PointF p1 = vertices[i + 1]; - - float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = (p0.Y - minY) + samplingOffsetY; - float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = (p1.Y - minY) + samplingOffsetY; - - if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) - { - continue; - } - - if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) - { - continue; - } - - int fx0 = FloatToFixed24Dot8(x0); - int fy0 = FloatToFixed24Dot8(y0); - int fx1 = FloatToFixed24Dot8(x1); - int fy1 = FloatToFixed24Dot8(y1); - if (fy0 == fy1) - { - continue; - } - - this.RasterizeLine(fx0, fy0, fx1, fy1); - } - } - } - - /// - /// Rasterizes all prebuilt edges that overlap this context. - /// - /// Shared edge table. - /// Top row of this context in global scanner-local coordinates. - public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) - { - int bandTopFixed = bandTop * FixedOne; - int bandBottomFixed = bandTopFixed + (this.height * FixedOne); - - for (int i = 0; i < edges.Length; i++) - { - EdgeData edge = edges[i]; - int x0 = edge.X0; - int y0 = edge.Y0; - int x1 = edge.X1; - int y1 = edge.Y1; - - if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) - { - continue; - } - - // Convert global scanner Y to band-local Y after clipping. - y0 -= bandTopFixed; - y1 -= bandTopFixed; - - this.RasterizeLine(x0, y0, x1, y1); - } - } - - /// - /// Rasterizes a subset of prebuilt edges that intersect this context's vertical range. - /// - /// Shared edge table. - /// Indices into for this band/tile. - /// Top row of this context in global scanner-local coordinates. - public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan edgeIndices, int bandTop) - { - int bandTopFixed = bandTop * FixedOne; - int bandBottomFixed = bandTopFixed + (this.height * FixedOne); - - for (int i = 0; i < edgeIndices.Length; i++) - { - EdgeData edge = edges[edgeIndices[i]]; - int x0 = edge.X0; - int y0 = edge.Y0; - int x1 = edge.X1; - int y1 = edge.Y1; - - if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) - { - continue; - } - - // Convert global scanner Y to band-local Y after clipping. - y0 -= bandTopFixed; - y1 -= bandTopFixed; - - this.RasterizeLine(x0, y0, x1, y1); - } - } - - /// - /// Converts accumulated cover/area tables into scanline coverage callbacks. - /// - /// The caller-owned mutable state type. - /// Absolute destination Y corresponding to row zero in this context. - /// Reusable scanline scratch buffer. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - public readonly void EmitScanlines(int destinationTop, Span scanline, ref TState state, RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - for (int row = 0; row < this.height; row++) - { - int rowCover = this.startCover[row]; - if (rowCover == 0 && this.rowHasBits[row] == 0) - { - // Nothing contributed to this row. - continue; - } - - Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); - scanline.Clear(); - bool scanlineDirty = this.EmitRowCoverage(rowBitVectors, row, rowCover, scanline); - if (scanlineDirty) - { - scanlineHandler(destinationTop + row, scanline, ref state); - } - } - } - - /// - /// Clears only rows touched during the previous rasterization pass. - /// - /// - /// This sparse reset strategy avoids clearing full scratch buffers when geometry is sparse. - /// - public void ResetTouchedRows() - { - // Reset only rows that received contributions in this band. This avoids clearing - // full temporary buffers when geometry is sparse relative to the interest bounds. - for (int i = 0; i < this.touchedRowCount; i++) - { - int row = this.touchedRows[i]; - this.startCover[row] = 0; - this.rowTouched[row] = 0; - - if (this.rowHasBits[row] == 0) - { - continue; - } - - this.rowHasBits[row] = 0; - this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow).Clear(); - } - - this.touchedRowCount = 0; - } - - /// - /// Emits one row by iterating touched columns and coalescing equal-coverage spans. - /// - /// Bitset words indicating touched columns in this row. - /// Row index inside the context. - /// Initial carry cover value from x less than zero contributions. - /// Destination scanline coverage buffer. - /// when at least one non-zero span was emitted. - private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) - { - int rowOffset = row * this.coverStride; - int spanStart = 0; - int spanEnd = 0; - float spanCoverage = 0F; - bool hasCoverage = false; - - for (int wordIndex = 0; wordIndex < rowBitVectors.Length; wordIndex++) - { - // Iterate touched columns sparsely by scanning set bits only. - nuint bitset = rowBitVectors[wordIndex]; - while (bitset != 0) - { - int localBitIndex = TrailingZeroCount(bitset); - bitset &= bitset - 1; - - int x = (wordIndex * WordBitCount) + localBitIndex; - if ((uint)x >= (uint)this.width) - { - continue; - } - - int tableIndex = rowOffset + (x << 1); - - // Area uses current cover before adding this cell's delta. This matches - // scan-conversion math where area integrates the edge state at cell entry. - int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); - float coverage = this.AreaToCoverage(area); - - if (spanEnd == x) - { - if (coverage <= 0F) - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x + 1; - spanEnd = spanStart; - spanCoverage = 0F; - } - else if (coverage == spanCoverage) - { - spanEnd = x + 1; - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - else - { - // We jumped over untouched columns. If cover != 0 the gap has a constant - // non-zero coverage and must be emitted as its own run. - if (cover == 0) - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - else - { - float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); - if (spanCoverage == gapCoverage) - { - if (coverage == gapCoverage) - { - spanEnd = x + 1; - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, x, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - hasCoverage |= FlushSpan(scanline, spanEnd, x, gapCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - } - - cover += this.coverArea[tableIndex]; - } - } - - // Flush tail run and any remaining constant-cover tail after the last touched cell. - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - if (cover != 0 && spanEnd < this.width) - { - hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); - } - - return hasCoverage; - } - - /// - /// Converts accumulated signed area to normalized coverage under the selected fill rule. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly float AreaToCoverage(int area) - { - int signedArea = area >> AreaToCoverageShift; - int absoluteArea = signedArea < 0 ? -signedArea : signedArea; - float coverage; - if (this.intersectionRule == IntersectionRule.NonZero) - { - // Non-zero winding clamps absolute winding accumulation to [0, 1]. - if (absoluteArea >= CoverageStepCount) - { - coverage = 1F; - } - else - { - coverage = absoluteArea * CoverageScale; - } - } - else - { - // Even-odd wraps every 2*CoverageStepCount and mirrors second half. - int wrapped = absoluteArea & EvenOddMask; - if (wrapped > CoverageStepCount) - { - wrapped = EvenOddPeriod - wrapped; - } - - coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; - } - - if (this.rasterizationMode == RasterizationMode.Aliased) - { - // Aliased mode quantizes final coverage to hard 0/1 per pixel. - return coverage >= 0.5F ? 1F : 0F; - } - - return coverage; - } - - /// - /// Writes one coverage span into the scanline buffer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool FlushSpan(Span scanline, int start, int end, float coverage) - { - if (coverage <= 0F || end <= start) - { - return false; - } - - scanline[start..end].Fill(coverage); - return true; - } - - /// - /// Sets a row/column bit and reports whether it was newly set. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly bool ConditionalSetBit(int row, int column) - { - int bitIndex = row * this.wordsPerRow; - int wordIndex = bitIndex + (column / WordBitCount); - nuint mask = (nuint)1 << (column % WordBitCount); - ref nuint word = ref this.bitVectors[wordIndex]; - bool newlySet = (word & mask) == 0; - word |= mask; - - // Fast row-level early-out for EmitScanlines. - this.rowHasBits[row] = 1; - return newlySet; - } - - /// - /// Adds one cell contribution into cover/area accumulators. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddCell(int row, int column, int delta, int area) - { - if ((uint)row >= (uint)this.height) - { - return; - } - - this.MarkRowTouched(row); - - if (column < 0) - { - // Contributions left of x=0 accumulate into the row carry. - this.startCover[row] += delta; - return; - } - - if ((uint)column >= (uint)this.width) - { - return; - } - - int index = (row * this.coverStride) + (column << 1); - if (this.ConditionalSetBit(row, column)) - { - // First write wins initialization path avoids reading old values. - this.coverArea[index] = delta; - this.coverArea[index + 1] = area; - } - else - { - // Multiple edges can hit the same cell; accumulate signed values. - this.coverArea[index] += delta; - this.coverArea[index + 1] += area; - } - } - - /// - /// Marks a row as touched once so sparse reset can clear it later. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void MarkRowTouched(int row) - { - if (this.rowTouched[row] != 0) - { - return; - } - - this.rowTouched[row] = 1; - this.touchedRows[this.touchedRowCount++] = row; - } - - /// - /// Emits one vertical cell contribution. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CellVertical(int px, int py, int x, int y0, int y1) - { - int delta = y0 - y1; - int area = delta * ((FixedOne * 2) - x - x); - this.AddCell(py, px, delta, area); - } - - /// - /// Emits one general cell contribution. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Cell(int row, int px, int x0, int y0, int x1, int y1) - { - int delta = y0 - y1; - int area = delta * ((FixedOne * 2) - x0 - x1); - this.AddCell(row, px, delta, area); - } - - /// - /// Rasterizes a downward vertical edge segment. - /// - private void VerticalDown(int columnIndex, int y0, int y1, int x) - { - int rowIndex0 = y0 >> FixedShift; - int rowIndex1 = (y1 - 1) >> FixedShift; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int fx = x - (columnIndex << FixedShift); - - if (rowIndex0 == rowIndex1) - { - // Entire segment stays within one row. - this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); - return; - } - - // First partial row, full middle rows, last partial row. - this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); - for (int row = rowIndex0 + 1; row < rowIndex1; row++) - { - this.CellVertical(columnIndex, row, fx, 0, FixedOne); - } - - this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); - } - - /// - /// Rasterizes an upward vertical edge segment. - /// - private void VerticalUp(int columnIndex, int y0, int y1, int x) - { - int rowIndex0 = (y0 - 1) >> FixedShift; - int rowIndex1 = y1 >> FixedShift; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int fx = x - (columnIndex << FixedShift); - - if (rowIndex0 == rowIndex1) - { - // Entire segment stays within one row. - this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); - return; - } - - // First partial row, full middle rows, last partial row (upward direction). - this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); - for (int row = rowIndex0 - 1; row > rowIndex1; row--) - { - this.CellVertical(columnIndex, row, fx, FixedOne, 0); - } - - this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); - } - - // The following row/line helpers are directional variants of the same fixed-point edge - // walker. They are intentionally split to minimize branch costs in hot loops. - - /// - /// Rasterizes a downward, left-to-right segment within a single row. - /// - private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = p0x >> FixedShift; - int columnIndex1 = (p1x - 1) >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p1x - p0x; - int dy = p1y - p0y; - int pp = (FixedOne - fx0) * dy; - int cy = p0y + (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); - - int idx = columnIndex0 + 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx++) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy + delta; - this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); - } - - /// - /// RowDownR variant that handles perfectly vertical edge ownership consistently. - /// - private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x < p1x) - { - this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - /// - /// Rasterizes an upward, left-to-right segment within a single row. - /// - private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = p0x >> FixedShift; - int columnIndex1 = (p1x - 1) >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p1x - p0x; - int dy = p0y - p1y; - int pp = (FixedOne - fx0) * dy; - int cy = p0y - (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); - - int idx = columnIndex0 + 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx++) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy - delta; - this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); - } - - /// - /// RowUpR variant that handles perfectly vertical edge ownership consistently. - /// - private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x < p1x) - { - this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - /// - /// Rasterizes a downward, right-to-left segment within a single row. - /// - private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = (p0x - 1) >> FixedShift; - int columnIndex1 = p1x >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p0x - p1x; - int dy = p1y - p0y; - int pp = fx0 * dy; - int cy = p0y + (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); - - int idx = columnIndex0 - 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx--) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy + delta; - this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); - } - - /// - /// RowDownL variant that handles perfectly vertical edge ownership consistently. - /// - private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x > p1x) - { - this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - /// - /// Rasterizes an upward, right-to-left segment within a single row. - /// - private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = (p0x - 1) >> FixedShift; - int columnIndex1 = p1x >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p0x - p1x; - int dy = p0y - p1y; - int pp = fx0 * dy; - int cy = p0y - (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); - - int idx = columnIndex0 - 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx--) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy - delta; - this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); - } - - /// - /// RowUpL variant that handles perfectly vertical edge ownership consistently. - /// - private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x > p1x) - { - this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - /// - /// Rasterizes a downward, left-to-right segment spanning multiple rows. - /// - private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x1 - x0; - int dy = y1 - y0; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - - // p/delta/mod/rem implement an integer DDA that advances x at row boundaries - // without per-row floating-point math. - int p = (FixedOne - fy0) * dx; - int delta = p / dy; - int cx = x0 + delta; - - this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); - - int row = rowIndex0 + 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row++) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx + delta; - this.RowDownR_V(row, cx, 0, nx, FixedOne); - cx = nx; - } - } - - this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); - } - - /// - /// Rasterizes an upward, left-to-right segment spanning multiple rows. - /// - private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x1 - x0; - int dy = y0 - y1; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - - // Upward version of the same integer DDA stepping as LineDownR. - int p = fy0 * dx; - int delta = p / dy; - int cx = x0 + delta; - - this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); - - int row = rowIndex0 - 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row--) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx + delta; - this.RowUpR_V(row, cx, FixedOne, nx, 0); - cx = nx; - } - } - - this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); - } - - /// - /// Rasterizes a downward, right-to-left segment spanning multiple rows. - /// - private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x0 - x1; - int dy = y1 - y0; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - - // Right-to-left variant of the integer DDA. - int p = (FixedOne - fy0) * dx; - int delta = p / dy; - int cx = x0 - delta; - - this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); - - int row = rowIndex0 + 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row++) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx - delta; - this.RowDownL_V(row, cx, 0, nx, FixedOne); - cx = nx; - } - } - - this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); - } - - /// - /// Rasterizes an upward, right-to-left segment spanning multiple rows. - /// - private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x0 - x1; - int dy = y0 - y1; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - - // Upward + right-to-left variant of the integer DDA. - int p = fy0 * dx; - int delta = p / dy; - int cx = x0 - delta; - - this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); - - int row = rowIndex0 - 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row--) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx - delta; - this.RowUpL_V(row, cx, FixedOne, nx, 0); - cx = nx; - } - } - - this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); - } - - /// - /// Dispatches a clipped edge to the correct directional fixed-point walker. - /// - private void RasterizeLine(int x0, int y0, int x1, int y1) - { - if (x0 == x1) - { - // Vertical edges need ownership adjustment to avoid double counting at cell seams. - int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; - if (y0 < y1) - { - this.VerticalDown(columnIndex, y0, y1, x0); - } - else - { - this.VerticalUp(columnIndex, y0, y1, x0); - } - - return; - } - - if (y0 < y1) - { - // Downward edges use inclusive top/exclusive bottom row mapping. - int rowIndex0 = y0 >> FixedShift; - int rowIndex1 = (y1 - 1) >> FixedShift; - if (rowIndex0 == rowIndex1) - { - int rowBase = rowIndex0 << FixedShift; - int localY0 = y0 - rowBase; - int localY1 = y1 - rowBase; - if (x0 < x1) - { - this.RowDownR(rowIndex0, x0, localY0, x1, localY1); - } - else - { - this.RowDownL(rowIndex0, x0, localY0, x1, localY1); - } - } - else if (x0 < x1) - { - this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); - } - else - { - this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); - } - - return; - } - - // Upward edges mirror the mapping to preserve winding consistency. - int upRowIndex0 = (y0 - 1) >> FixedShift; - int upRowIndex1 = y1 >> FixedShift; - if (upRowIndex0 == upRowIndex1) - { - int rowBase = upRowIndex0 << FixedShift; - int localY0 = y0 - rowBase; - int localY1 = y1 - rowBase; - if (x0 < x1) - { - this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); - } - else - { - this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); - } - } - else if (x0 < x1) - { - this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); - } - else - { - this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); - } - } - } - - /// - /// Immutable scanner-local edge record with precomputed affected-row bounds. - /// - /// - /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path - /// access without per-read unpacking. - /// - private readonly struct EdgeData - { - /// - /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int X0; - - /// - /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int Y0; - - /// - /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int X1; - - /// - /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int Y1; - - /// - /// Gets the first scanner row affected by this edge. - /// - public readonly int MinRow; - - /// - /// Gets the last scanner row affected by this edge. - /// - public readonly int MaxRow; - - /// - /// Initializes a new instance of the struct. - /// - public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) - { - this.X0 = x0; - this.Y0 = y0; - this.X1 = x1; - this.Y1 = y1; - this.MinRow = minRow; - this.MaxRow = maxRow; - } - } - - /// - /// Mutable state used while capturing one tile's emitted scanlines. - /// - private readonly struct TileCaptureState - { - /// - /// Initializes a new instance of the struct. - /// - public TileCaptureState(int width, Memory coverage, Memory dirtyRows) - { - this.Top = 0; - this.Width = width; - this.Coverage = coverage; - this.DirtyRows = dirtyRows; - } - - /// - /// Gets the row origin of this capture buffer. - /// - public int Top { get; } - - /// - /// Gets the scanline width. - /// - public int Width { get; } - - /// - /// Gets contiguous tile coverage storage. - /// - public Memory Coverage { get; } - - /// - /// Gets per-row dirty flags for sparse output emission. - /// - public Memory DirtyRows { get; } - } - - /// - /// Buffered output produced by one rasterized tile. - /// - private sealed class TileOutput : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - public TileOutput(int top, int height, IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) - { - this.Top = top; - this.Height = height; - this.CoverageOwner = coverageOwner; - this.DirtyRowsOwner = dirtyRowsOwner; - } - - /// - /// Gets the tile top row relative to interest origin. - /// - public int Top { get; } - - /// - /// Gets the number of rows in this tile. - /// - public int Height { get; } - - /// - /// Gets the tile coverage buffer owner. - /// - public IMemoryOwner CoverageOwner { get; private set; } - - /// - /// Gets the tile dirty-row buffer owner. - /// - public IMemoryOwner DirtyRowsOwner { get; private set; } - - /// - /// Releases tile output buffers back to the allocator. - /// - public void Dispose() - { - this.CoverageOwner?.Dispose(); - this.DirtyRowsOwner?.Dispose(); - this.CoverageOwner = null!; - this.DirtyRowsOwner = null!; - } - } - - /// - /// Reusable per-worker scratch buffers used by tiled and sequential band rasterization. - /// - private sealed class WorkerScratch : IDisposable - { - private readonly int wordsPerRow; - private readonly int coverStride; - private readonly int width; - private readonly int tileCapacity; - private readonly IMemoryOwner bitVectorsOwner; - private readonly IMemoryOwner coverAreaOwner; - private readonly IMemoryOwner startCoverOwner; - private readonly IMemoryOwner rowHasBitsOwner; - private readonly IMemoryOwner rowTouchedOwner; - private readonly IMemoryOwner touchedRowsOwner; - private readonly IMemoryOwner scanlineOwner; - - private WorkerScratch( - int wordsPerRow, - int coverStride, - int width, - int tileCapacity, - IMemoryOwner bitVectorsOwner, - IMemoryOwner coverAreaOwner, - IMemoryOwner startCoverOwner, - IMemoryOwner rowHasBitsOwner, - IMemoryOwner rowTouchedOwner, - IMemoryOwner touchedRowsOwner, - IMemoryOwner scanlineOwner) - { - this.wordsPerRow = wordsPerRow; - this.coverStride = coverStride; - this.width = width; - this.tileCapacity = tileCapacity; - this.bitVectorsOwner = bitVectorsOwner; - this.coverAreaOwner = coverAreaOwner; - this.startCoverOwner = startCoverOwner; - this.rowHasBitsOwner = rowHasBitsOwner; - this.rowTouchedOwner = rowTouchedOwner; - this.touchedRowsOwner = touchedRowsOwner; - this.scanlineOwner = scanlineOwner; - } - - /// - /// Gets reusable scanline scratch for this worker. - /// - public Span Scanline => this.scanlineOwner.Memory.Span; - - /// - /// Allocates worker-local scratch sized for the configured tile/band capacity. - /// - public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, int coverStride, int width, int tileCapacity) - { - int bitVectorCapacity = checked(wordsPerRow * tileCapacity); - int coverAreaCapacity = checked(coverStride * tileCapacity); - IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); - IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); - IMemoryOwner startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner touchedRowsOwner = allocator.Allocate(tileCapacity); - IMemoryOwner scanlineOwner = allocator.Allocate(width); - - return new WorkerScratch( - wordsPerRow, - coverStride, - width, - tileCapacity, - bitVectorsOwner, - coverAreaOwner, - startCoverOwner, - rowHasBitsOwner, - rowTouchedOwner, - touchedRowsOwner, - scanlineOwner); - } - - /// - /// Creates a context view over this scratch for the requested band height. - /// - public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode) - { - if ((uint)bandHeight > (uint)this.tileCapacity) - { - ThrowBandHeightExceedsScratchCapacity(); - } - - int bitVectorCount = checked(this.wordsPerRow * bandHeight); - int coverAreaCount = checked(this.coverStride * bandHeight); - return new Context( - this.bitVectorsOwner.Memory.Span[..bitVectorCount], - this.coverAreaOwner.Memory.Span[..coverAreaCount], - this.startCoverOwner.Memory.Span[..bandHeight], - this.rowHasBitsOwner.Memory.Span[..bandHeight], - this.rowTouchedOwner.Memory.Span[..bandHeight], - this.touchedRowsOwner.Memory.Span[..bandHeight], - this.width, - bandHeight, - this.wordsPerRow, - this.coverStride, - intersectionRule, - rasterizationMode); - } - - /// - /// Releases worker-local scratch buffers back to the allocator. - /// - public void Dispose() - { - this.bitVectorsOwner.Dispose(); - this.coverAreaOwner.Dispose(); - this.startCoverOwner.Dispose(); - this.rowHasBitsOwner.Dispose(); - this.rowTouchedOwner.Dispose(); - this.touchedRowsOwner.Dispose(); - this.scanlineOwner.Dispose(); - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD index 8ca955856..a91abfd57 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD @@ -1,7 +1,7 @@ -# Polygon Scanner (Fixed-Point, Tiled + Banded Fallback) +# DefaultRasterizer (Fixed-Point, Tiled + Banded Fallback) -This document describes the current `PolygonScanner` implementation in -`src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs`. +This document describes the current `DefaultRasterizer` implementation in +`src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs`. The scanner is a fixed-point, area/cover rasterizer inspired by Blaze-style scan conversion. diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs new file mode 100644 index 000000000..fda958fe4 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Delegate invoked for each emitted non-zero coverage span. +/// +/// The destination y coordinate. +/// The first x coordinate represented by . +/// Non-zero coverage values starting at . +internal delegate void RasterizerCoverageRowHandler(int y, int startX, Span coverage); diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs deleted file mode 100644 index 6cd07f57a..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerScanlineHandler{TState}.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Delegate invoked for each rasterized scanline. -/// -/// The caller-provided state type. -/// The destination y coordinate. -/// Coverage values for the scanline. -/// Caller-provided mutable state. -internal delegate void RasterizerScanlineHandler(int y, Span scanline, ref TState state) - where TState : struct; diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs deleted file mode 100644 index d26a49847..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Single-pass CPU scanline rasterizer. -/// -/// -/// This implementation directly rasterizes the whole interest rectangle in one pass. -/// It is retained as a compact fallback/reference implementation and as an explicit -/// non-tiled option for profiling and comparison. -/// -internal sealed class ScanlineRasterizer -{ - /// - /// Gets the singleton scanline rasterizer instance. - /// - public static ScanlineRasterizer Instance { get; } = new(); - - /// - /// Rasterizes the path into scanline coverage using the sequential scanner path. - /// - public void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); - Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); - - Rectangle interest = options.Interest; - if (interest.Equals(Rectangle.Empty)) - { - return; - } - - PolygonScanner.RasterizeSequential(path, options, allocator, ref state, scanlineHandler); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs index 9b9059425..3e3b161a4 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs @@ -12,12 +12,31 @@ public void EmitsCoverageForSubpixelThinRectangle() { RectangularPolygon path = new(0.3F, 0.2F, 0.7F, 1.423F); RasterizerOptions options = new(new Rectangle(0, 0, 12, 20), IntersectionRule.EvenOdd); - CaptureState state = new(new float[options.Interest.Width * options.Interest.Height], options.Interest.Width, options.Interest.Top); + float[] coverage = new float[options.Interest.Width * options.Interest.Height]; + int width = options.Interest.Width; + int top = options.Interest.Top; + int dirtyRows = 0; + float maxCoverage = 0F; + + DefaultRasterizer.RasterizeRows(path, options, Configuration.Default.MemoryAllocator, CaptureRow); - DefaultRasterizer.Instance.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + Assert.True(dirtyRows > 0); + Assert.True(maxCoverage > 0F); + + void CaptureRow(int y, int startX, Span rowCoverage) + { + int row = y - top; + rowCoverage.CopyTo(coverage.AsSpan((row * width) + startX, rowCoverage.Length)); + dirtyRows++; - Assert.True(state.DirtyRows > 0); - Assert.True(state.MaxCoverage > 0F); + for (int i = 0; i < rowCoverage.Length; i++) + { + if (rowCoverage[i] > maxCoverage) + { + maxCoverage = rowCoverage[i]; + } + } + } } [Fact] @@ -26,7 +45,7 @@ public void RasterizesFractionalRectangleCoverageDeterministically() RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero); - float[] coverage = Rasterize(DefaultRasterizer.Instance, path, options); + float[] coverage = Rasterize(path, options); float[] expected = [ 0.5625F, 0.1875F, @@ -45,7 +64,7 @@ public void AliasedMode_EmitsBinaryCoverage() RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero, RasterizationMode.Aliased); - float[] coverage = Rasterize(DefaultRasterizer.Instance, path, options); + float[] coverage = Rasterize(path, options); float[] expected = [ 1F, 0F, @@ -60,69 +79,31 @@ public void ThrowsForInterestTooWideForCoverStrideMath() { RectangularPolygon path = new(0F, 0F, 1F, 1F); RasterizerOptions options = new(new Rectangle(0, 0, (int.MaxValue / 2) + 1, 1), IntersectionRule.NonZero); - NoopState state = default; void Rasterize() => - DefaultRasterizer.Instance.Rasterize( + DefaultRasterizer.RasterizeRows( path, options, Configuration.Default.MemoryAllocator, - ref state, - static (int y, Span scanline, ref NoopState localState) => { }); + static (int y, int startX, Span coverage) => { }); ImageProcessingException exception = Assert.Throws(Rasterize); Assert.Contains("too large", exception.Message); } - private static float[] Rasterize(DefaultRasterizer rasterizer, IPath path, in RasterizerOptions options) + private static float[] Rasterize(IPath path, in RasterizerOptions options) { int width = options.Interest.Width; int height = options.Interest.Height; float[] coverage = new float[width * height]; - CaptureState state = new(coverage, width, options.Interest.Top); - - rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + int top = options.Interest.Top; + DefaultRasterizer.RasterizeRows(path, options, Configuration.Default.MemoryAllocator, CaptureRow); return coverage; - } - - private static void CaptureScanline(int y, Span scanline, ref CaptureState state) - { - int row = y - state.Top; - scanline.CopyTo(state.Coverage.AsSpan(row * state.Width, state.Width)); - state.DirtyRows++; - - for (int i = 0; i < scanline.Length; i++) - { - if (scanline[i] > state.MaxCoverage) - { - state.MaxCoverage = scanline[i]; - } - } - } - private struct CaptureState - { - public CaptureState(float[] coverage, int width, int top) + void CaptureRow(int y, int startX, Span rowCoverage) { - this.Coverage = coverage; - this.Width = width; - this.Top = top; - this.DirtyRows = 0; - this.MaxCoverage = 0F; + int row = y - top; + rowCoverage.CopyTo(coverage.AsSpan((row * width) + startX, rowCoverage.Length)); } - - public float[] Coverage { get; } - - public int Width { get; } - - public int Top { get; } - - public int DirtyRows { get; set; } - - public float MaxCoverage { get; set; } - } - - private struct NoopState - { } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs index eb9a28064..93fce0ec7 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs @@ -34,8 +34,8 @@ public void MatchesDefaultRasterizer_ForLargeSelfIntersectingPath(IntersectionRu Rectangle interest = Rectangle.Ceiling(path.Bounds); RasterizerOptions options = new(interest, rule); - float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); - float[] actual = Rasterize(DefaultRasterizer.Instance, path, options); + float[] expected = RasterizeSequential(path, options); + float[] actual = Rasterize(path, options); AssertCoverageEqual(expected, actual); } @@ -50,40 +50,44 @@ public void MatchesDefaultRasterizer_ForPixelCenterSampling() IntersectionRule.NonZero, samplingOrigin: RasterizerSamplingOrigin.PixelCenter); - float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); - float[] actual = Rasterize(DefaultRasterizer.Instance, path, options); + float[] expected = RasterizeSequential(path, options); + float[] actual = Rasterize(path, options); AssertCoverageEqual(expected, actual); } - private static float[] Rasterize(DefaultRasterizer rasterizer, IPath path, in RasterizerOptions options) + private static float[] Rasterize(IPath path, in RasterizerOptions options) { int width = options.Interest.Width; int height = options.Interest.Height; float[] coverage = new float[width * height]; - CaptureState state = new(coverage, width, options.Interest.Top); - - rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + int top = options.Interest.Top; + DefaultRasterizer.RasterizeRows(path, options, Configuration.Default.MemoryAllocator, CaptureRow); return coverage; + + void CaptureRow(int y, int startX, Span rowCoverage) + { + int row = y - top; + rowCoverage.CopyTo(coverage.AsSpan((row * width) + startX, rowCoverage.Length)); + } } - private static float[] Rasterize(ScanlineRasterizer rasterizer, IPath path, in RasterizerOptions options) + private static float[] RasterizeSequential(IPath path, in RasterizerOptions options) { int width = options.Interest.Width; int height = options.Interest.Height; float[] coverage = new float[width * height]; - CaptureState state = new(coverage, width, options.Interest.Top); - - rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + int top = options.Interest.Top; + DefaultRasterizer.RasterizeRowsSequential(path, options, Configuration.Default.MemoryAllocator, CaptureRow); return coverage; - } - private static void CaptureScanline(int y, Span scanline, ref CaptureState state) - { - int row = y - state.Top; - scanline.CopyTo(state.Coverage.AsSpan(row * state.Width, state.Width)); + void CaptureRow(int y, int startX, Span rowCoverage) + { + int row = y - top; + rowCoverage.CopyTo(coverage.AsSpan((row * width) + startX, rowCoverage.Length)); + } } private static void AssertCoverageEqual(ReadOnlySpan expected, ReadOnlySpan actual) @@ -94,20 +98,4 @@ private static void AssertCoverageEqual(ReadOnlySpan expected, ReadOnlySp Assert.Equal(expected[i], actual[i], 6); } } - - private readonly struct CaptureState - { - public CaptureState(float[] coverage, int width, int top) - { - this.Coverage = coverage; - this.Width = width; - this.Top = top; - } - - public float[] Coverage { get; } - - public int Width { get; } - - public int Top { get; } - } } diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index 907d91489..494e80369 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -7,8 +7,8 @@ lcov [SixLabors.*]* - - ^Clipper2Lib\..* + + ^SixLabors.ImageSharp.Drawing.WebGPU\..* true From 35c4445dd4baf5a7f09d7ca16c7dc575c143f8e0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 11:34:03 +1000 Subject: [PATCH 082/136] Feng shui all the things. --- README.md | 8 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 1 - .../WebGPUDrawingBackend.cs | 20 +-- .../{Shapes => }/ArcLineSegment.cs | 0 .../{Shapes => }/BooleanOperation.cs | 0 .../{Shapes => }/ClipPathExtensions.cs | 2 +- .../{Shapes => }/ComplexPolygon.cs | 0 .../{Shapes => }/CubicBezierLineSegment.cs | 5 +- .../{Shapes => }/EllipsePolygon.cs | 0 .../{Shapes => }/EmptyPath.cs | 0 .../{Shapes => }/Helpers/ArrayExtensions.cs | 14 +- .../Helpers/PolygonUtilities.cs | 125 ++++++++++++++ .../Helpers/ThreadLocalBlenderBuffers.cs | 124 ++++++++++++++ .../{Shapes => }/IInternalPathOwner.cs | 0 .../{Shapes => }/ILineSegment.cs | 0 src/ImageSharp.Drawing/{Shapes => }/IPath.cs | 0 .../{Shapes => }/IPathCollection.cs | 0 .../{Shapes => }/IPathInternals.cs | 0 .../{Shapes => }/ISimplePath.cs | 0 .../{Shapes => }/InnerJoin.cs | 0 .../{Shapes => }/InternalPath.cs | 40 ++--- .../{Shapes => }/IntersectionRule.cs | 0 .../{Shapes => }/LineCap.cs | 0 .../{Shapes => }/LineJoin.cs | 0 .../{Shapes => }/LinearLineSegment.cs | 1 + .../{Shapes => }/OutlinePathExtensions.cs | 2 +- src/ImageSharp.Drawing/{Shapes => }/Path.cs | 0 .../{Shapes => }/PathBuilder.cs | 0 .../{Shapes => }/PathCollection.cs | 0 .../{Shapes => }/PathExtensions.Internal.cs | 0 .../{Shapes => }/PathExtensions.cs | 0 .../{Shapes => }/PathTypes.cs | 0 .../{Shapes => }/PointOrientation.cs | 0 .../{Shapes => }/Polygon.cs | 0 .../PolygonGeometry/ClippedShapeGenerator.cs | 2 +- .../PolygonGeometry/PolygonClipperFactory.cs | 2 +- .../PolygonGeometry/StrokedShapeGenerator.cs | 2 +- .../Processing/Backends/CompositionCommand.cs | 1 - .../Backends/CompositionCoverageDefinition.cs | 2 - .../Backends/DefaultDrawingBackend.cs | 1 - .../Backends}/DefaultRasterizer.cs | 2 +- .../Processing/Backends/IDrawingBackend.cs | 1 - .../Backends}/PolygonScanning.MD | 0 .../Backends}/RasterizerCoverageRowHandler.cs | 2 +- .../Backends}/RasterizerOptions.cs | 2 +- .../Processing/DrawingCanvas{TPixel}.cs | 1 - .../Processing/GradientBrush.cs | 2 +- .../Processing/PathGradientBrush.cs | 4 +- .../Processing/PatternBrush.cs | 4 +- .../Processing/RecolorBrush.cs | 2 +- .../Processing/SolidBrush.cs | 2 +- .../{Shapes => }/RectangularPolygon.cs | 0 .../{Shapes => }/RegularPolygon.cs | 0 .../{Shapes => }/SegmentInfo.cs | 0 .../Shapes/Helpers/ArrayBuilder{T}.cs | 156 ------------------ .../Shapes/Helpers/TopologyUtilities.cs | 49 ------ .../Shapes/Helpers/VectorExtensions.cs | 44 ----- src/ImageSharp.Drawing/{Shapes => }/Star.cs | 0 .../{Shapes => }/TessellatedMultipolygon.cs | 6 +- .../{Shapes => }/Text/BaseGlyphBuilder.cs | 0 .../{Shapes => }/Text/GlyphBuilder.cs | 0 .../{Shapes => }/Text/GlyphLayerInfo.cs | 0 .../{Shapes => }/Text/GlyphLayerKind.cs | 0 .../{Shapes => }/Text/GlyphPathCollection.cs | 0 .../{Shapes => }/Text/PathGlyphBuilder.cs | 0 .../{Shapes => }/Text/TextBuilder.cs | 0 .../{Shapes => }/Text/TextUtilities.cs | 0 src/ImageSharp.Drawing/Utilities/Intersect.cs | 78 --------- .../Utilities/NumericUtilities.cs | 56 ------- .../Utilities/ThreadLocalBlenderBuffers.cs | 65 -------- .../ImageSharp.Drawing.Benchmarks.csproj | 11 +- .../Helpers/PolygonUtilitiesTests.cs | 96 +++++++++++ .../ThreadLocalBlenderBuffersTests.cs | 6 +- .../ImageSharp.Drawing.Tests.csproj | 2 +- .../{Shapes => }/Issues/Issue_19.cs | 2 +- .../{Shapes => }/Issues/Issue_224.cs | 2 +- .../PolygonClippingTests.cs} | 7 +- .../Backends/SkiaCoverageDrawingBackend.cs | 1 - .../Processing/DrawingCanvasBatcherTests.cs | 5 +- .../Processing/DrawingCanvasTests.Process.cs | 1 - .../RasterizerDefaultsExtensionsTests.cs | 1 - .../DefaultRasterizerRegressionTests.cs | 8 +- .../DefaultRasterizerTests.cs | 4 +- .../IntersectionsGenerator.py | 0 .../Rasterization/NumericCornerCases.jpg | 3 + .../SimplePolygon_AllEmitCases.png | 3 + .../TessellatedMultipolygonTests.cs | 4 +- .../Shapes/InternalPathTests.cs | 2 + .../Shapes/PolygonTests.cs | 2 + .../Shapes/RectangleTests.cs | 1 + .../Shapes/Scan/NumericCornerCases.jpg | Bin 623151 -> 0 bytes .../Scan/SimplePolygon_AllEmitCases.png | Bin 28411 -> 0 bytes .../Shapes/Scan/TopologyUtilitiesTests.cs | 59 ------- .../Structs => TestUtilities}/TestPoint.cs | 2 +- .../Structs => TestUtilities}/TestSize.cs | 2 +- .../Utilities/IntersectTests.cs | 45 ----- .../Utilities/NumericUtilitiesTests.cs | 28 ---- tests/coverlet.runsettings | 9 +- 98 files changed, 454 insertions(+), 680 deletions(-) rename src/ImageSharp.Drawing/{Shapes => }/ArcLineSegment.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/BooleanOperation.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/ClipPathExtensions.cs (97%) rename src/ImageSharp.Drawing/{Shapes => }/ComplexPolygon.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/CubicBezierLineSegment.cs (98%) rename src/ImageSharp.Drawing/{Shapes => }/EllipsePolygon.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/EmptyPath.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Helpers/ArrayExtensions.cs (68%) create mode 100644 src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs create mode 100644 src/ImageSharp.Drawing/Helpers/ThreadLocalBlenderBuffers.cs rename src/ImageSharp.Drawing/{Shapes => }/IInternalPathOwner.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/ILineSegment.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/IPath.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/IPathCollection.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/IPathInternals.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/ISimplePath.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/InnerJoin.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/InternalPath.cs (93%) rename src/ImageSharp.Drawing/{Shapes => }/IntersectionRule.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/LineCap.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/LineJoin.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/LinearLineSegment.cs (98%) rename src/ImageSharp.Drawing/{Shapes => }/OutlinePathExtensions.cs (99%) rename src/ImageSharp.Drawing/{Shapes => }/Path.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PathBuilder.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PathCollection.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PathExtensions.Internal.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PathExtensions.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PathTypes.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PointOrientation.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Polygon.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/PolygonGeometry/ClippedShapeGenerator.cs (98%) rename src/ImageSharp.Drawing/{Shapes => }/PolygonGeometry/PolygonClipperFactory.cs (97%) rename src/ImageSharp.Drawing/{Shapes => }/PolygonGeometry/StrokedShapeGenerator.cs (98%) rename src/ImageSharp.Drawing/{Shapes/Rasterization => Processing/Backends}/DefaultRasterizer.cs (99%) rename src/ImageSharp.Drawing/{Shapes/Rasterization => Processing/Backends}/PolygonScanning.MD (100%) rename src/ImageSharp.Drawing/{Shapes/Rasterization => Processing/Backends}/RasterizerCoverageRowHandler.cs (89%) rename src/ImageSharp.Drawing/{Shapes/Rasterization => Processing/Backends}/RasterizerOptions.cs (97%) rename src/ImageSharp.Drawing/{Shapes => }/RectangularPolygon.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/RegularPolygon.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/SegmentInfo.cs (100%) delete mode 100644 src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs rename src/ImageSharp.Drawing/{Shapes => }/Star.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/TessellatedMultipolygon.cs (96%) rename src/ImageSharp.Drawing/{Shapes => }/Text/BaseGlyphBuilder.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/GlyphBuilder.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/GlyphLayerInfo.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/GlyphLayerKind.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/GlyphPathCollection.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/PathGlyphBuilder.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/TextBuilder.cs (100%) rename src/ImageSharp.Drawing/{Shapes => }/Text/TextUtilities.cs (100%) delete mode 100644 src/ImageSharp.Drawing/Utilities/Intersect.cs delete mode 100644 src/ImageSharp.Drawing/Utilities/NumericUtilities.cs delete mode 100644 src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs rename tests/ImageSharp.Drawing.Tests/{Utilities => Helpers}/ThreadLocalBlenderBuffersTests.cs (94%) rename tests/ImageSharp.Drawing.Tests/{Shapes => }/Issues/Issue_19.cs (96%) rename tests/ImageSharp.Drawing.Tests/{Shapes => }/Issues/Issue_224.cs (97%) rename tests/ImageSharp.Drawing.Tests/{Shapes/PolygonClipper/ClipperTests.cs => PolygonGeometry/PolygonClippingTests.cs} (95%) rename tests/ImageSharp.Drawing.Tests/{Shapes/Scan => Rasterization}/DefaultRasterizerRegressionTests.cs (94%) rename tests/ImageSharp.Drawing.Tests/{Shapes/Scan => Rasterization}/DefaultRasterizerTests.cs (96%) rename tests/ImageSharp.Drawing.Tests/{Shapes/Scan => Rasterization}/IntersectionsGenerator.py (100%) create mode 100644 tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg create mode 100644 tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png rename tests/ImageSharp.Drawing.Tests/{Shapes/Scan => Rasterization}/TessellatedMultipolygonTests.cs (96%) delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCases.jpg delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/TopologyUtilitiesTests.cs rename tests/ImageSharp.Drawing.Tests/{Shapes/Structs => TestUtilities}/TestPoint.cs (94%) rename tests/ImageSharp.Drawing.Tests/{Shapes/Structs => TestUtilities}/TestSize.cs (94%) delete mode 100644 tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs diff --git a/README.md b/README.md index b5a3413ee..ef44ddf34 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ SixLabors.ImageSharp.Drawing -### **ImageSharp.Drawing** provides extensions to ImageSharp containing powerful, cross-platform 2D polygon manipulation and drawing APIs. +### **ImageSharp.Drawing** provides extensions to ImageSharp containing powerful, Cross-Platform 2D polygon manipulation and drawing APIs. Designed to democratize image processing, ImageSharp.Drawing brings you an incredibly powerful yet beautifully simple API. -Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. +Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. ## License @@ -61,12 +61,12 @@ If you prefer, you can compile ImageSharp.Drawing yourself (please do and help!) - Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) - Make sure you have the latest version installed - - Make sure you have [the .NET 7 SDK](https://www.microsoft.com/net/core#windows) installed + - Make sure you have [the .NET 8 SDK](https://www.microsoft.com/net/core#windows) installed Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**: - [Visual Studio Code](https://code.visualstudio.com/) with [C# Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) -- [the .NET 7 SDK](https://www.microsoft.com/net/core#linuxubuntu) +- [the .NET 8 SDK](https://www.microsoft.com/net/core#linuxubuntu) To clone ImageSharp.Drawing locally, click the "Clone in [YOUR_OS]" button above or run the following git commands: diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 106c70356..7bc45f908 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 75f5e96f9..2d7956485 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -7,8 +7,6 @@ using System.Runtime.InteropServices; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -508,8 +506,8 @@ private bool TryRenderPreparedFlush( outputOriginY = 0; } - List coverageDefinitions = new(); - Dictionary coverageDefinitionIndexByKey = new(); + List coverageDefinitions = []; + Dictionary coverageDefinitionIndexByKey = []; int[] batchCoverageIndices = new int[preparedBatches.Count]; for (int i = 0; i < batchCoverageIndices.Length; i++) { @@ -628,7 +626,7 @@ private bool TryDispatchPreparedCompositeCommands( } string pipelineKey = $"prepared-composite-fine/{flushContext.TextureFormat}"; - WebGPUCompositeBindGroupLayoutFactory layoutFactory = (WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) => TryCreatePreparedCompositeFineBindGroupLayout( api, device, @@ -640,7 +638,7 @@ private bool TryDispatchPreparedCompositeCommands( if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( pipelineKey, shaderCode, - layoutFactory, + LayoutFactory, out BindGroupLayout* bindGroupLayout, out ComputePipeline* pipeline, out error)) @@ -790,8 +788,8 @@ private bool TryDispatchPreparedCompositeCommands( destinationX + destinationRegion.Width, destinationY + destinationRegion.Height); commandIndex++; + } } - } int usedParameterByteCount = checked(flushCommandCount * (int)parameterSize); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( @@ -852,7 +850,7 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - nuint binDataByteCount = checked((nuint)binningSize * (nuint)sizeof(uint)); + nuint binDataByteCount = checked(binningSize * (nuint)sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeBinDataBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, @@ -918,8 +916,8 @@ private bool TryDispatchPreparedCompositeCommands( (nuint)tileCountsByteCount); uint tileCommandCapacity = maxTileCommandIndices; - nuint usedTileCommandCount = (nuint)Math.Max(tileCommandCapacity, 1u); - nuint tileCommandIndicesByteCount = checked(usedTileCommandCount * (nuint)sizeof(uint)); + nuint usedTileCommandCount = Math.Max(tileCommandCapacity, 1u); + nuint tileCommandIndicesByteCount = checked(usedTileCommandCount * sizeof(uint)); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeTileIndicesBufferKey, BufferUsage.Storage | BufferUsage.CopyDst, @@ -1874,7 +1872,7 @@ private static uint DivideRoundUp(int value, int divisor) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint FloatToUInt32Bits(float value) - => unchecked((uint)System.BitConverter.SingleToInt32Bits(value)); + => unchecked((uint)BitConverter.SingleToInt32Bits(value)); /// /// Finalizes one flush by submitting command buffers and optionally reading results back to CPU memory. diff --git a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs b/src/ImageSharp.Drawing/ArcLineSegment.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs rename to src/ImageSharp.Drawing/ArcLineSegment.cs diff --git a/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs b/src/ImageSharp.Drawing/BooleanOperation.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/BooleanOperation.cs rename to src/ImageSharp.Drawing/BooleanOperation.cs diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/ClipPathExtensions.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs rename to src/ImageSharp.Drawing/ClipPathExtensions.cs index b9b3ccde9..9ad53bbff 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/ClipPathExtensions.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/ComplexPolygon.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs rename to src/ImageSharp.Drawing/ComplexPolygon.cs diff --git a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs rename to src/ImageSharp.Drawing/CubicBezierLineSegment.cs index e9a44bd37..d655caacc 100644 --- a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; @@ -54,7 +55,7 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control /// public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end) - : this(new[] { start, controlPoint1, controlPoint2, end }) + : this([start, controlPoint1, controlPoint2, end]) { } @@ -119,7 +120,7 @@ private static PointF[] GetDrawingPoints(PointF[] controlPoints) drawingPoints.AddRange(bezierCurveDrawingPoints); } - return drawingPoints.ToArray(); + return [.. drawingPoints]; } private static List FindDrawingPoints(int curveIndex, PointF[] controlPoints) diff --git a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs b/src/ImageSharp.Drawing/EllipsePolygon.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs rename to src/ImageSharp.Drawing/EllipsePolygon.cs diff --git a/src/ImageSharp.Drawing/Shapes/EmptyPath.cs b/src/ImageSharp.Drawing/EmptyPath.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/EmptyPath.cs rename to src/ImageSharp.Drawing/EmptyPath.cs diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs similarity index 68% rename from src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs rename to src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs index 1e2e9c103..33a658455 100644 --- a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs +++ b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs @@ -1,20 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing; +namespace SixLabors.ImageSharp.Drawing.Helpers; /// -/// Extensions on arrays. +/// Extension methods for arrays. /// internal static class ArrayExtensions { /// - /// Merges the specified source2. + /// Merges two arrays into one. /// /// the type of the array - /// The source1. - /// The source2. - /// the Merged arrays + /// The first source array. + /// The second source array. + /// + /// A new array containing the elements of both source arrays. + /// public static T[] Merge(this T[] source1, T[] source2) { if (source2 is null || source2.Length == 0) diff --git a/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs new file mode 100644 index 000000000..e035fc940 --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing; + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Provides low-level geometry helpers for polygon winding and segment intersection. +/// +/// +/// Polygon methods expect a closed ring where the first point is repeated as the last point. +/// Orientation signs are defined using world-space math conventions (Y points up): +/// positive signed area is counter-clockwise and negative signed area is clockwise. +/// In screen space (Y points down), the visual winding appears inverted. +/// +internal static class PolygonUtilities +{ + // Epsilon used for floating-point tolerance. Values within +-Eps are treated as zero. + // This reduces instability when segments are nearly parallel or endpoints are close. + private const float Eps = 1e-3f; + private const float MinusEps = -Eps; + private const float OnePlusEps = 1 + Eps; + + /// + /// Ensures that a closed polygon ring matches the expected orientation. + /// + /// Polygon ring to normalize in place. + /// + /// Expected orientation sign: + /// positive for counter-clockwise in world space, negative for clockwise in world space. + /// + /// + /// The ring is reversed only when its orientation sign disagrees with + /// . Degenerate rings (zero area) are not changed. + /// + public static void EnsureOrientation(Span polygon, int expectedOrientation) + { + if (GetPolygonOrientation(polygon) * expectedOrientation < 0) + { + polygon.Reverse(); + } + } + + /// + /// Returns the orientation sign of a closed polygon ring using the shoelace sum. + /// + /// Closed polygon ring. + /// + /// -1 for clockwise, 1 for counter-clockwise, or 0 for degenerate (zero-area) input. + /// + private static int GetPolygonOrientation(ReadOnlySpan polygon) + { + float sum = 0f; + for (int i = 0; i < polygon.Length - 1; ++i) + { + PointF current = polygon[i]; + PointF next = polygon[i + 1]; + sum += (current.X * next.Y) - (next.X * current.Y); + } + + // A tolerant compare could be used here, but edge scanning does not special-case + // zero-area or near-zero-area input, so we keep this strict sign check. + return Math.Sign(sum); + } + + /// + /// Tests whether two line segments intersect, excluding collinear overlap cases. + /// + /// Start point of segment A. + /// End point of segment A. + /// Start point of segment B. + /// End point of segment B. + /// + /// Receives the intersection point when an intersection is found. + /// If no intersection is detected, the value is not modified. + /// + /// + /// when the segments intersect within their extents + /// (including endpoints); otherwise . + /// + /// + /// This solves the two segment equations in parametric form and accepts values in [0, 1] + /// with an epsilon margin for floating-point tolerance. + /// Parallel and collinear pairs are rejected early (cross product ~= 0). + /// + public static bool LineSegmentToLineSegmentIgnoreCollinear( + Vector2 a0, + Vector2 a1, + Vector2 b0, + Vector2 b1, + ref Vector2 intersectionPoint) + { + // Direction vectors of the segments. + float dax = a1.X - a0.X; + float day = a1.Y - a0.Y; + float dbx = b1.X - b0.X; + float dby = b1.Y - b0.Y; + + // Cross product of the direction vectors. Near zero means parallel/collinear. + float crossD = (-dbx * day) + (dax * dby); + + // Reject parallel and collinear lines. Collinear overlap is intentionally not handled. + if (crossD is > MinusEps and < Eps) + { + return false; + } + + // Solve for parameters s and t where: + // a0 + t * (a1 - a0) = b0 + s * (b1 - b0) + float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD; + float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD; + + // If both parameters are within [0,1] (with tolerance), the segments intersect. + if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps) + { + intersectionPoint.X = a0.X + (t * dax); + intersectionPoint.Y = a0.Y + (t * day); + return true; + } + + return false; + } +} diff --git a/src/ImageSharp.Drawing/Helpers/ThreadLocalBlenderBuffers.cs b/src/ImageSharp.Drawing/Helpers/ThreadLocalBlenderBuffers.cs new file mode 100644 index 000000000..1468843eb --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/ThreadLocalBlenderBuffers.cs @@ -0,0 +1,124 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Provides per-thread scratch buffers used by brush applicators during blending. +/// +/// The target pixel type. +/// +/// +/// Each participating thread gets its own pair of scanline-sized buffers: +/// one for blend amounts ( values) and, optionally, one for overlay pixels. +/// +/// +/// This avoids per-row allocations while preventing cross-thread contention on shared buffers. +/// +/// +/// Instances must be disposed to release all thread-local allocations. +/// +/// +internal class ThreadLocalBlenderBuffers : IDisposable + where TPixel : unmanaged, IPixel +{ + private readonly ThreadLocal data; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used to create per-thread buffers. + /// The required buffer length, in pixels. + /// + /// to allocate only the amount buffer. + /// Use this when blending does not require an intermediate overlay color buffer. + /// + public ThreadLocalBlenderBuffers(MemoryAllocator allocator, int scanlineWidth, bool amountBufferOnly = false) + => this.data = new ThreadLocal(() => new BufferOwner(allocator, scanlineWidth, amountBufferOnly), true); + + /// + /// Gets the current thread's amount buffer. + /// + /// + /// The span length is equal to the configured scanline width. + /// The returned span is thread-local and should only be used on the calling thread. + /// + public Span AmountSpan => this.data.Value!.AmountSpan; + + /// + /// Gets the current thread's overlay color buffer. + /// + /// + /// When the instance was created with amountBufferOnly=true, + /// this property returns an empty span. + /// + public Span OverlaySpan => this.data.Value!.OverlaySpan; + + /// + public void Dispose() + { + foreach (BufferOwner d in this.data.Values) + { + d.Dispose(); + } + + this.data.Dispose(); + } + + /// + /// Owns the actual memory buffers for a single thread. + /// + private sealed class BufferOwner : IDisposable + { + private readonly IMemoryOwner amountBuffer; + private readonly IMemoryOwner? overlayBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for memory ownership. + /// The required buffer length, in pixels. + /// + /// to omit overlay buffer allocation. + /// + public BufferOwner(MemoryAllocator allocator, int scanlineLength, bool amountBufferOnly) + { + this.amountBuffer = allocator.Allocate(scanlineLength); + this.overlayBuffer = amountBufferOnly ? null : allocator.Allocate(scanlineLength); + } + + /// + /// Gets the per-thread amount buffer. + /// + public Span AmountSpan => this.amountBuffer.Memory.Span; + + /// + /// Gets the per-thread overlay buffer. + /// + /// + /// Returns an empty span when overlay storage was intentionally not allocated. + /// + public Span OverlaySpan + { + get + { + if (this.overlayBuffer != null) + { + return this.overlayBuffer.Memory.Span; + } + + return []; + } + } + + /// + public void Dispose() + { + this.amountBuffer.Dispose(); + this.overlayBuffer?.Dispose(); + } + } +} diff --git a/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs b/src/ImageSharp.Drawing/IInternalPathOwner.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs rename to src/ImageSharp.Drawing/IInternalPathOwner.cs diff --git a/src/ImageSharp.Drawing/Shapes/ILineSegment.cs b/src/ImageSharp.Drawing/ILineSegment.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/ILineSegment.cs rename to src/ImageSharp.Drawing/ILineSegment.cs diff --git a/src/ImageSharp.Drawing/Shapes/IPath.cs b/src/ImageSharp.Drawing/IPath.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IPath.cs rename to src/ImageSharp.Drawing/IPath.cs diff --git a/src/ImageSharp.Drawing/Shapes/IPathCollection.cs b/src/ImageSharp.Drawing/IPathCollection.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IPathCollection.cs rename to src/ImageSharp.Drawing/IPathCollection.cs diff --git a/src/ImageSharp.Drawing/Shapes/IPathInternals.cs b/src/ImageSharp.Drawing/IPathInternals.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IPathInternals.cs rename to src/ImageSharp.Drawing/IPathInternals.cs diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/ISimplePath.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/ISimplePath.cs rename to src/ImageSharp.Drawing/ISimplePath.cs diff --git a/src/ImageSharp.Drawing/Shapes/InnerJoin.cs b/src/ImageSharp.Drawing/InnerJoin.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/InnerJoin.cs rename to src/ImageSharp.Drawing/InnerJoin.cs diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs similarity index 93% rename from src/ImageSharp.Drawing/Shapes/InternalPath.cs rename to src/ImageSharp.Drawing/InternalPath.cs index cc6a53ea0..1af0919fd 100644 --- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -20,11 +20,6 @@ internal class InternalPath private const float Epsilon = 0.003f; private const float Epsilon2 = 0.2f; - /// - /// The maximum vector - /// - private static readonly Vector2 MaxVector = new(float.MaxValue); - /// /// The points. /// @@ -170,7 +165,7 @@ internal SegmentInfo PointAlongPath(float distanceAlongPath) // For open paths we're going to create a new virtual point that extends past the path. // The position and angle for that point are calculated based upon the last two points. PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point; - PointF b = this.points[this.points.Length - 1].Point; + PointF b = this.points[^1].Point; Vector2 delta = a - b; float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); @@ -217,21 +212,6 @@ private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vecto return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointOrientation CalculateOrientation(Vector2 qp, Vector2 rq) - { - // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ - // for details of below formula. - float val = (qp.Y * rq.X) - (qp.X * rq.Y); - - if (val > -Epsilon && val < Epsilon) - { - return PointOrientation.Collinear; // colinear - } - - return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise - } - /// /// Simplifies the collection of segments. /// @@ -294,7 +274,7 @@ private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, return [.. results]; } } - while (removeCloseAndCollinear && points[0].Equivalent(points[prev], Epsilon2)); // skip points too close together + while (removeCloseAndCollinear && Equivalent(points[0], points[prev], Epsilon2)); // skip points too close together polyCorners = prev + 1; lastPoint = points[prev]; @@ -341,6 +321,22 @@ private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, return [.. results]; } + /// + /// Merges the specified source2. + /// + /// The source1. + /// The source2. + /// The threshold. + /// + /// the Merged arrays + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Equivalent(PointF source1, PointF source2, float threshold) + { + Vector2 abs = Vector2.Abs(source1 - source2); + return abs.X < threshold && abs.Y < threshold; + } + private struct PointData { public PointF Point; diff --git a/src/ImageSharp.Drawing/Shapes/IntersectionRule.cs b/src/ImageSharp.Drawing/IntersectionRule.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IntersectionRule.cs rename to src/ImageSharp.Drawing/IntersectionRule.cs diff --git a/src/ImageSharp.Drawing/Shapes/LineCap.cs b/src/ImageSharp.Drawing/LineCap.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/LineCap.cs rename to src/ImageSharp.Drawing/LineCap.cs diff --git a/src/ImageSharp.Drawing/Shapes/LineJoin.cs b/src/ImageSharp.Drawing/LineJoin.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/LineJoin.cs rename to src/ImageSharp.Drawing/LineJoin.cs diff --git a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs b/src/ImageSharp.Drawing/LinearLineSegment.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs rename to src/ImageSharp.Drawing/LinearLineSegment.cs index f1170baf3..303482ec3 100644 --- a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs +++ b/src/ImageSharp.Drawing/LinearLineSegment.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/OutlinePathExtensions.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs rename to src/ImageSharp.Drawing/OutlinePathExtensions.cs index 78ff4fc15..cd4c08011 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/OutlinePathExtensions.cs @@ -2,8 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Path.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Path.cs rename to src/ImageSharp.Drawing/Path.cs diff --git a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathBuilder.cs rename to src/ImageSharp.Drawing/PathBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/PathCollection.cs b/src/ImageSharp.Drawing/PathCollection.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathCollection.cs rename to src/ImageSharp.Drawing/PathCollection.cs diff --git a/src/ImageSharp.Drawing/Shapes/PathExtensions.Internal.cs b/src/ImageSharp.Drawing/PathExtensions.Internal.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathExtensions.Internal.cs rename to src/ImageSharp.Drawing/PathExtensions.Internal.cs diff --git a/src/ImageSharp.Drawing/Shapes/PathExtensions.cs b/src/ImageSharp.Drawing/PathExtensions.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathExtensions.cs rename to src/ImageSharp.Drawing/PathExtensions.cs diff --git a/src/ImageSharp.Drawing/Shapes/PathTypes.cs b/src/ImageSharp.Drawing/PathTypes.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathTypes.cs rename to src/ImageSharp.Drawing/PathTypes.cs diff --git a/src/ImageSharp.Drawing/Shapes/PointOrientation.cs b/src/ImageSharp.Drawing/PointOrientation.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PointOrientation.cs rename to src/ImageSharp.Drawing/PointOrientation.cs diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Polygon.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Polygon.cs rename to src/ImageSharp.Drawing/Polygon.cs diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs rename to src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs index d423b57aa..6f2e36f75 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs @@ -5,7 +5,7 @@ using PCPolygon = SixLabors.PolygonClipper.Polygon; using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// Generates clipped shapes from one or more input paths using polygon boolean operations. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs rename to src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs index dfe11f4d4..454820488 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs @@ -4,7 +4,7 @@ using SixLabors.PolygonClipper; using PCPolygon = SixLabors.PolygonClipper.Polygon; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// Builders for from ImageSharp paths. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs rename to src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs index a3dc75836..a37f427cd 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -6,7 +6,7 @@ using PCPolygon = SixLabors.PolygonClipper.Polygon; using StrokeOptions = SixLabors.ImageSharp.Drawing.Processing.StrokeOptions; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// Generates stroked and merged shapes using polygon stroking and boolean clipping. diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index c5c3be603..c6f6ce237 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs index 3bb609749..f3973ccae 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 9cefb8068..1d7dc6366 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs rename to src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 3c4aa61ad..2ff6a8170 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Default fixed-point rasterizer that converts polygon edges into per-row coverage. diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 438657502..73deea4e8 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD rename to src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs similarity index 89% rename from src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs rename to src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs index fda958fe4..405c2cd76 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerCoverageRowHandler.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Delegate invoked for each emitted non-zero coverage span. diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs rename to src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 66a8cfbb7..6b627530c 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Describes whether rasterizers should emit continuous coverage or binary aliased coverage. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 55678d28c..2ceedda8a 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -9,7 +9,6 @@ using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors.Transforms; diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 9aafa9082..3d029523c 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index 3e47f6c70..745c7efe4 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -157,7 +157,7 @@ public bool Intersect( Vector2 start, Vector2 end, ref Vector2 ip) => - Utilities.Intersect.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); + PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); public Vector4 ColorAt(float distance) { diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index ff5a328dc..e115d863d 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -162,7 +162,7 @@ public override void Apply(Span scanline, int x, int y) for (int i = 0; i < scanline.Length; i++) { - amounts[i] = NumericUtilities.ClampFloat(scanline[i] * this.Options.BlendPercentage, 0, 1F); + amounts[i] = Math.Clamp(scanline[i] * this.Options.BlendPercentage, 0, 1F); int patternX = (x + i) % this.pattern.Columns; overlays[i] = this.pattern[patternY, patternX]; diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index 1592c6448..a4fb37a2e 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index dc944d68a..7a23caad1 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/RectangularPolygon.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs rename to src/ImageSharp.Drawing/RectangularPolygon.cs diff --git a/src/ImageSharp.Drawing/Shapes/RegularPolygon.cs b/src/ImageSharp.Drawing/RegularPolygon.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/RegularPolygon.cs rename to src/ImageSharp.Drawing/RegularPolygon.cs diff --git a/src/ImageSharp.Drawing/Shapes/SegmentInfo.cs b/src/ImageSharp.Drawing/SegmentInfo.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/SegmentInfo.cs rename to src/ImageSharp.Drawing/SegmentInfo.cs diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs deleted file mode 100644 index c8e7cc26e..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers; - -/// -/// A helper type for avoiding allocations while building arrays. -/// -/// The type of item contained in the array. -internal struct ArrayBuilder - where T : struct -{ - private const int DefaultCapacity = 4; - - // Starts out null, initialized on first Add. - private T[]? data; - private int size; - - /// - /// Initializes a new instance of the struct. - /// - /// The initial capacity of the array. - public ArrayBuilder(int capacity) - : this() - { - if (capacity > 0) - { - this.data = new T[capacity]; - } - } - - /// - /// Gets or sets the number of items in the array. - /// - public int Length - { - readonly get => this.size; - - set - { - if (value > 0) - { - this.EnsureCapacity(value); - this.size = value; - } - else - { - this.size = 0; - } - } - } - - /// - /// Returns a reference to specified element of the array. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public readonly ref T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index)); - return ref this.data![index]; - } - } - - /// - /// Adds the given item to the array. - /// - /// The item to add. - public void Add(T item) - { - int position = this.size; - T[]? array = this.data; - - if (array != null && (uint)position < (uint)array.Length) - { - this.size = position + 1; - array[position] = item; - } - else - { - this.AddWithResize(item); - } - } - - // Non-inline from Add to improve its code quality as uncommon path - [MethodImpl(MethodImplOptions.NoInlining)] - private void AddWithResize(T item) - { - int size = this.size; - this.Grow(size + 1); - this.size = size + 1; - this.data[size] = item; - } - - /// - /// Remove the last item from the array. - /// - public void RemoveLast() - { - DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size)); - this.size--; - } - - /// - /// Clears the array. - /// Allocated memory is left intact for future usage. - /// - public void Clear() => - - // No need to actually clear since we're not allowing reference types. - this.size = 0; - - private void EnsureCapacity(int min) - { - int length = this.data?.Length ?? 0; - if (length < min) - { - this.Grow(min); - } - } - - [MemberNotNull(nameof(this.data))] - private void Grow(int capacity) - { - // Same expansion algorithm as List. - int length = this.data?.Length ?? 0; - int newCapacity = length == 0 ? DefaultCapacity : length * 2; - if ((uint)newCapacity > Array.MaxLength) - { - newCapacity = Array.MaxLength; - } - - if (newCapacity < capacity) - { - newCapacity = capacity; - } - - T[] array = new T[newCapacity]; - - if (this.size > 0) - { - Array.Copy(this.data!, array, this.size); - } - - this.data = array; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs b/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs deleted file mode 100644 index 6f5c2f401..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers; - -/// -/// Implements some basic algorithms on raw data structures. -/// Polygons are represented with a span of points, -/// where first point should be repeated at the end. -/// -/// -/// Positive orientation means Clockwise in world coordinates (positive direction goes UP on paper). -/// Since the Drawing library deals mostly with Screen coordinates where this is opposite, -/// we use different terminology here to avoid confusion. -/// -internal static class TopologyUtilities -{ - /// - /// Positive: CCW in world coords (CW on screen) - /// Negative: CW in world coords (CCW on screen) - /// - public static void EnsureOrientation(Span polygon, int expectedOrientation) - { - if (GetPolygonOrientation(polygon) * expectedOrientation < 0) - { - polygon.Reverse(); - } - } - - /// - /// Zero: area is 0 - /// Positive: CCW in world coords (CW on screen) - /// Negative: CW in world coords (CCW on screen) - /// - private static int GetPolygonOrientation(ReadOnlySpan polygon) - { - float sum = 0f; - for (int i = 0; i < polygon.Length - 1; ++i) - { - PointF curr = polygon[i]; - PointF next = polygon[i + 1]; - sum += (curr.X * next.Y) - (next.X * curr.Y); - } - - // Normally, this should be a tolerant comparison, we don't have a special path for zero-area - // (or for self-intersecting, semi-zero-area) polygons in edge scanning. - return Math.Sign(sum); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs deleted file mode 100644 index 849c93e79..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Extensions on arrays. -/// -internal static class VectorExtensions -{ - /// - /// Merges the specified source2. - /// - /// The source1. - /// The source2. - /// The threshold. - /// - /// the Merged arrays - /// - public static bool Equivalent(this PointF source1, PointF source2, float threshold) - { - Vector2 abs = Vector2.Abs(source1 - source2); - - return abs.X < threshold && abs.Y < threshold; - } - - /// - /// Merges the specified source2. - /// - /// The source1. - /// The source2. - /// The threshold. - /// - /// the Merged arrays - /// - public static bool Equivalent(this Vector2 source1, Vector2 source2, float threshold) - { - Vector2 abs = Vector2.Abs(source1 - source2); - - return abs.X < threshold && abs.Y < threshold; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Star.cs b/src/ImageSharp.Drawing/Star.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Star.cs rename to src/ImageSharp.Drawing/Star.cs diff --git a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs b/src/ImageSharp.Drawing/TessellatedMultipolygon.cs similarity index 96% rename from src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs rename to src/ImageSharp.Drawing/TessellatedMultipolygon.cs index eade34439..5c495ecff 100644 --- a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs +++ b/src/ImageSharp.Drawing/TessellatedMultipolygon.cs @@ -3,10 +3,10 @@ using System.Buffers; using System.Collections; -using SixLabors.ImageSharp.Drawing.Shapes.Helpers; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Drawing.Shapes; +namespace SixLabors.ImageSharp.Drawing; /// /// Compact representation of a multipolygon. @@ -91,7 +91,7 @@ static void RepeatFirstVertexAndEnsureOrientation(Span span, bool enforc if (enforcePositiveOrientation) { - TopologyUtilities.EnsureOrientation(span, 1); + PolygonUtilities.EnsureOrientation(span, 1); } } } diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs rename to src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs b/src/ImageSharp.Drawing/Text/GlyphBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs rename to src/ImageSharp.Drawing/Text/GlyphBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs rename to src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphLayerKind.cs b/src/ImageSharp.Drawing/Text/GlyphLayerKind.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphLayerKind.cs rename to src/ImageSharp.Drawing/Text/GlyphLayerKind.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs rename to src/ImageSharp.Drawing/Text/GlyphPathCollection.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs rename to src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/TextBuilder.cs b/src/ImageSharp.Drawing/Text/TextBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/TextBuilder.cs rename to src/ImageSharp.Drawing/Text/TextBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/TextUtilities.cs b/src/ImageSharp.Drawing/Text/TextUtilities.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/TextUtilities.cs rename to src/ImageSharp.Drawing/Text/TextUtilities.cs diff --git a/src/ImageSharp.Drawing/Utilities/Intersect.cs b/src/ImageSharp.Drawing/Utilities/Intersect.cs deleted file mode 100644 index e20ed9eb9..000000000 --- a/src/ImageSharp.Drawing/Utilities/Intersect.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing.Utilities; - -/// -/// Lightweight 2D segment intersection helpers for polygon and path processing. -/// -/// -/// This is intentionally small and allocation-free. It favors speed and numerical tolerance -/// over exhaustive classification (e.g., collinear overlap detection), which keeps it fast -/// enough for per-segment scanning in stroking or clipping preparation passes. -/// -internal static class Intersect -{ - // Epsilon used for floating-point tolerance. We treat values within ±Eps as zero. - // This helps avoid instability when segments are nearly parallel or endpoints are - // very close to the intersection boundary. - private const float Eps = 1e-3f; - private const float MinusEps = -Eps; - private const float OnePlusEps = 1 + Eps; - - /// - /// Tests two line segments for intersection, ignoring collinear overlap. - /// - /// Start of segment A. - /// End of segment A. - /// Start of segment B. - /// End of segment B. - /// - /// Receives the intersection point when the segments intersect within tolerance. - /// When no intersection is detected, the value is left unchanged. - /// - /// - /// if the segments intersect within their extents (including endpoints), - /// if they are disjoint or collinear. - /// - /// - /// The method is based on solving two parametric line equations and uses a small epsilon - /// window around [0, 1] to account for floating-point error. Collinear cases are rejected - /// early (crossD ≈ 0) to keep the method fast; callers that need collinear overlap detection - /// must implement that separately. - /// - public static bool LineSegmentToLineSegmentIgnoreCollinear(Vector2 a0, Vector2 a1, Vector2 b0, Vector2 b1, ref Vector2 intersectionPoint) - { - // Direction vectors of the segments. - float dax = a1.X - a0.X; - float day = a1.Y - a0.Y; - float dbx = b1.X - b0.X; - float dby = b1.Y - b0.Y; - - // Cross product of directions. When near zero, the lines are parallel or collinear. - float crossD = (-dbx * day) + (dax * dby); - - // Reject parallel/collinear lines. Collinear overlap is intentionally ignored. - if (crossD is > MinusEps and < Eps) - { - return false; - } - - // Solve for parameters s and t where: - // a0 + t*(a1-a0) = b0 + s*(b1-b0) - float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD; - float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD; - - // If both parameters are within [0,1] (with tolerance), the segments intersect. - if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps) - { - intersectionPoint.X = a0.X + (t * dax); - intersectionPoint.Y = a0.Y + (t * day); - return true; - } - - return false; - } -} diff --git a/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs b/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs deleted file mode 100644 index b2401ddf9..000000000 --- a/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Drawing.Utilities; - -internal static class NumericUtilities -{ - public static void AddToAllElements(this Span span, float value) - { - ref float current = ref MemoryMarshal.GetReference(span); - ref float max = ref Unsafe.Add(ref current, span.Length); - - if (Vector.IsHardwareAccelerated) - { - int n = span.Length / Vector.Count; - ref Vector currentVec = ref Unsafe.As>(ref current); - ref Vector maxVec = ref Unsafe.Add(ref currentVec, n); - - Vector vecVal = new(value); - while (Unsafe.IsAddressLessThan(ref currentVec, ref maxVec)) - { - currentVec += vecVal; - currentVec = ref Unsafe.Add(ref currentVec, 1); - } - - // current = ref Unsafe.Add(ref current, n * Vector.Count); - current = ref Unsafe.As, float>(ref currentVec); - } - - while (Unsafe.IsAddressLessThan(ref current, ref max)) - { - current += value; - current = ref Unsafe.Add(ref current, 1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float ClampFloat(float value, float min, float max) - { - if (value >= max) - { - return max; - } - - if (value <= min) - { - return min; - } - - return value; - } -} diff --git a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs b/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs deleted file mode 100644 index c3a07c111..000000000 --- a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Utilities; - -internal class ThreadLocalBlenderBuffers : IDisposable - where TPixel : unmanaged, IPixel -{ - private readonly ThreadLocal data; - - // amountBufferOnly:true is for SolidBrush, which doesn't need the overlay buffer (it will be dummy) - public ThreadLocalBlenderBuffers(MemoryAllocator allocator, int scanlineWidth, bool amountBufferOnly = false) - => this.data = new ThreadLocal(() => new BufferOwner(allocator, scanlineWidth, amountBufferOnly), true); - - public Span AmountSpan => this.data.Value!.AmountSpan; - - public Span OverlaySpan => this.data.Value!.OverlaySpan; - - /// - public void Dispose() - { - foreach (BufferOwner d in this.data.Values) - { - d.Dispose(); - } - - this.data.Dispose(); - } - - private sealed class BufferOwner : IDisposable - { - private readonly IMemoryOwner amountBuffer; - private readonly IMemoryOwner? overlayBuffer; - - public BufferOwner(MemoryAllocator allocator, int scanlineLength, bool amountBufferOnly) - { - this.amountBuffer = allocator.Allocate(scanlineLength); - this.overlayBuffer = amountBufferOnly ? null : allocator.Allocate(scanlineLength); - } - - public Span AmountSpan => this.amountBuffer.Memory.Span; - - public Span OverlaySpan - { - get - { - if (this.overlayBuffer != null) - { - return this.overlayBuffer.Memory.Span; - } - - return []; - } - } - - public void Dispose() - { - this.amountBuffer.Dispose(); - this.overlayBuffer?.Dispose(); - } - } -} diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index f85acbe30..5b9dd9a66 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -9,11 +9,16 @@ - + + - CA1822 - CA1416 + + + CA1822;CA1416;CA1001;CS0029;CA1861;CA2201 diff --git a/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs b/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs new file mode 100644 index 000000000..7c2aa4f20 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing.Tests.Helpers; + +public class PolygonUtilitiesTests +{ + private static PointF[] CreateTestPoints() + => PolygonFactory.CreatePointArray( + (10, 0), + (20, 0), + (20, 30), + (10, 30), + (10, 20), + (0, 20), + (0, 10), + (10, 10), + (10, 0)); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnsureOrientation_Positive(bool isPositive) + { + PointF[] expected = CreateTestPoints(); + PointF[] polygon = expected.CloneArray(); + + if (!isPositive) + { + polygon.AsSpan().Reverse(); + } + + PolygonUtilities.EnsureOrientation(polygon, 1); + + Assert.Equal(expected, polygon); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnsureOrientation_Negative(bool isNegative) + { + PointF[] expected = CreateTestPoints(); + expected.AsSpan().Reverse(); + + PointF[] polygon = expected.CloneArray(); + + if (!isNegative) + { + polygon.AsSpan().Reverse(); + } + + PolygonUtilities.EnsureOrientation(polygon, -1); + + Assert.Equal(expected, polygon); + } + + public static TheoryData<(float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y)?> LineSegmentToLineSegment_Data { get; } = + new() + { + { (0, 0), (2, 3), (1, 3), (1, 0), (1, 1.5f) }, + { (3, 1), (3, 3), (3, 2), (4, 2), (3, 2) }, + { (1, -3), (3, -1), (3, -4), (2, -2), (2, -2) }, + { (0, 0), (2, 1), (2, 1.0001f), (5, 2), (2, 1) }, // Robust to inaccuracies + { (0, 0), (2, 3), (1, 3), (1, 2), null }, + { (-3, 3), (-1, 3), (-3, 2), (-1, 2), null }, + { (-4, 3), (-4, 1), (-5, 3), (-5, 1), null }, + { (0, 0), (4, 1), (4, 1), (8, 2), null }, // Collinear intersections are ignored + { (0, 0), (4, 1), (4, 1.0001f), (8, 2), null }, // Collinear intersections are ignored + }; + + [Theory] + [MemberData(nameof(LineSegmentToLineSegment_Data))] + public void LineSegmentToLineSegmentNoCollinear( + (float X, float Y) a0, + (float X, float Y) a1, + (float X, float Y) b0, + (float X, float Y) b1, + (float X, float Y)? expected) + { + Vector2 ip = default; + + bool result = PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(P(a0), P(a1), P(b0), P(b1), ref ip); + Assert.Equal(result, expected.HasValue); + if (expected.HasValue) + { + Assert.Equal(P(expected.Value), ip, new ApproximateFloatComparer(1e-3f)); + } + + static Vector2 P((float X, float Y) p) => new(p.X, p.Y); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs b/tests/ImageSharp.Drawing.Tests/Helpers/ThreadLocalBlenderBuffersTests.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs rename to tests/ImageSharp.Drawing.Tests/Helpers/ThreadLocalBlenderBuffersTests.cs index 42d7e5983..c21cd931a 100644 --- a/tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Helpers/ThreadLocalBlenderBuffersTests.cs @@ -1,10 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; +namespace SixLabors.ImageSharp.Drawing.Tests.Helpers; public class ThreadLocalBlenderBuffersTests { diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 9553c38f1..da30dc625 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -32,7 +32,7 @@ - + diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs similarity index 96% rename from tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs rename to tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs index d920fa9d0..21fbd5784 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs @@ -3,7 +3,7 @@ using System.Numerics; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; /// /// see https://github.com/issues/19 diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs similarity index 97% rename from tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs rename to tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs index aa9391361..c682f1935 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; /// /// see https://github.com/SixLabors/ImageSharp.Drawing/issues/224 diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs similarity index 95% rename from tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs rename to tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs index 7943ac2df..4e741462c 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs @@ -2,12 +2,13 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; -namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Tests.PolygonGeometry; -public class ClipperTests +public class PolygonClippingTests { private readonly RectangularPolygon bigSquare = new(10, 10, 40, 40); private readonly RectangularPolygon hole = new(20, 20, 10, 10); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 67cdb6376..14e628206 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SkiaSharp; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 81c365489..12885d426 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -112,7 +111,7 @@ public void FlushCompositions( return; } - this.LastBatch = batches[batches.Count - 1]; + this.LastBatch = batches[^1]; this.HasBatch = true; this.Batches.AddRange(batches); } @@ -121,7 +120,7 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) + [NotNullWhen(true)] out Image image) where TPixel : unmanaged, IPixel { image = null; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index a8bc7d8d9..13cbe4642 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index e62d612cf..8e0c69900 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs rename to tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs index 3e3b161a4..2fa41c661 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerRegressionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs @@ -1,9 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Rasterization; public class DefaultRasterizerRegressionTests { diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs similarity index 96% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs rename to tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs index 93fce0ec7..f8c6e35b2 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs @@ -2,9 +2,9 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Drawing.Processing.Backends; -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; +namespace SixLabors.ImageSharp.Drawing.Tests.Rasterization; public class DefaultRasterizerTests { diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/IntersectionsGenerator.py b/tests/ImageSharp.Drawing.Tests/Rasterization/IntersectionsGenerator.py similarity index 100% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/IntersectionsGenerator.py rename to tests/ImageSharp.Drawing.Tests/Rasterization/IntersectionsGenerator.py diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg b/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg new file mode 100644 index 000000000..4d47bc457 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14120aec3ece697f2e69559b542c59fb4008083d1e9300e9174700223551645e +size 623151 diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png b/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png new file mode 100644 index 000000000..fb6d09308 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99b523663db7a64a03a836719b54cf64ded5a1128317a95282e866cb9f368ab4 +size 28411 diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs similarity index 96% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs rename to tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs index 6dae4fb1d..7daea40a1 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs @@ -1,10 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Shapes; +using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; +namespace SixLabors.ImageSharp.Drawing.Tests.Rasterization; public class TessellatedMultipolygonTests { diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs index 3ed572163..846c14de0 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; /// diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs index 3af46a7a8..e1c4a5f89 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; public class PolygonTests diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs index 9ea049e3e..f5d9ca383 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCases.jpg b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCases.jpg deleted file mode 100644 index 91bdf70fdfd24a5f9d3266a29b2bd0ca9b7af707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 623151 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 p`D2mCK-j70w@Pvt_nc2n(^y>4 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�LFFetp^UaXO=c(DPCQ8 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 z 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�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 SJGm7jbUn*@_P>td%AT0n2L|P3T3wMPP|= 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 z zcs^2tPv_hZ%+KHJJgJ$)I}a%~h6n=WI07bAS^3vPzE0AmWASc7$Nfg((reSauDCmn z$r04R>whjMH<&~|nMS_YBfskIrMLahN$)jdyTg{~KP)EU_0UfhQJt-lUB2_uIVA+h z(5}sUxo9|oodm17o&Q&a|JQ9Ro5a{87$%CJG<`KEZHiSF%eyT7c-|@}+uWk1EKUM#~e{D?? z10Z=IvNDiu4}SR{t(NAaqy;JQoJ2!^b;uK74&nZJFCQ#yZCz?HT34dC;E)a_@-MC@;6XgM}qY@X63kXgB1(zx-3~ z^@R5n1(o|fxD&BB^x`^O)_%BIcSt(9^mM%g9kt^~6{q}ooIBx?r;JIxqwTX@xh`V) z53(BP_nZruYFnU<%By((pb4E~?tT498da~BO>H;S2g$&96G`%y{ciU8WWX<<<=PT| z27bf9Cv8tTdvQ*3U9;(m&ByBKE)@$cv$s`|B-%L0?tDdt^}e)_k6dW*T+DQ~6Tk z(DcWu)0(Y`~kqFtQ=51WgflSkZ>|Mv7$l7`o(|`=nJ$`%xM7Xq8!7v3hlc+i@sL z=x=P=x}ARfl%jQxw?}kW6IuHODq?;7+!_00mz@X~;y58Unzo0Ggs>(KvbOacEd55s zp&Q7a8L3&qBN911+nL`gda{+eupt*4>>GCG%u?4m04nXQ^IIy0AnJ_R0@mi{|ABN6 zGmL}65ajzffzjSR=snRgz8MKHLZdxUobxz{TP;J>m8Nz;n&D!cE#S9cPdrMCfI19Z zLS6bWiq;s&$J_q42>DkIZRIn;G>k56E- z%|qw2S}tWUY7``rUs`X;K$Z`l!ARrjBixUo*)`ETJ?Hc&!_3))ER`3?z;O_xzd)6bI(Ya$Tg$tq83c$6HG^nR%fg^~mMf z1w&G$XM(0;4REea@Tut@4m^o*v)N_oD$q;l z2IHz7C*}Y8+V`v#jSpAR43_hB>2ODl%AHflnekVJp(9hX77|(_Lkp>3}t%1+G*Zv;m9ae4=4dOw;}*>RVb z;=-V+=)%JGciBu`e*0If6ZKx(V@*GSX}biur{z0>ewsYXi#UsFBMHY?Dn%bAPa6G( zBsLS!*V``Le+y#0K8ptL@-eQ?rtB2zJ{K^}doeLsLuj*&SiSvyh@EJ^O^b-4O=ZIf z2?I2-FYc#QyZny);K&4b{#NUeH z^qCrr4Q3_rk^5A|rs=^wnGTd$+AwRuaXN=hC50jQV|=}?XF4RJyy>0O54Y_jz@9`$ zJ8dtHXSt$l)zA&dFJr0lyR+MC&voF(cGPUJjr16)BaJ?ZwE&I@@~Aokz-b^0>KI?H zso4ZMq@3Z?>Sn%}n@k7@eud9rbDq5-KRWJ>3VVgj;XS|o=TMbOyB_b7<+EuHD7gdg zzjK@xHba5BK`}jCnFK>hc?)zXj-Dojjj+tOJ$QM$|7vN$bUdz7HQbE<^+nIyP|ZaE zcA7{G%}iMEtuC~5%8I<#ypwovjxG5-dOa+}e1P>U3lppvKb2N?ES~FU{_Az6+_nV# zi@dFy3q02;Jz?n&ynoGTn)Ad@#PH6_`NZ9|dqFgeum!6zOx){=L(9|GD#m>$jALf# zTXhJ^4tC3P^~f8XuOPJcxwxb zCJYOUS3&H?|J+PW^&A;}7;~HLx@DVzLkpN({ozdY{J<79rUHf)tg^?9(5ciT*+6W0 z-o7&DH&@`ktq2j*R8gdz(DYe^;GE~je?2NyQ4l<4VCTttNcx3irZQfDL`%GuE@0k# zc!NMjZx+CB1Sc;f9s1BNI{?nlXFO946*MV_0xTh}87WEzxl~4)D85}HBg#|sf=l)k zwYR*lh-gYZm*FtQ?>_&^hqg|Yex)n_Ca=u%$fLuW>-%#qHO_XJ81D|I3a@qS3i}1s zySsEGNM0|r&$s(RnXcjrx{xcnXXl@NEgF5*tb2PTt2}# zjnH!10vL(^ki7p@vg;j@NZsOo3`BY(-u?T4e@b3rR!@n0N*?f=0+#vO^cx72=<1N4 z4ZSx&Y*p^ygr)!Mh|Og~V1f;{sXvH)E$<>=A>V4ghJi76BY0`*|6V8$jJ)-Zh(%F( zFwNf+`d(m9CiH=UJ1ETm@79jBTJn3n+qJ2Aj|gC#{ow^l{l&L$MYf+$CZ3no5AK%n z-;%^-$hGaeAbF}v?u`6hKM(9OqIPBSds7l+e`QoxroQCmg!)kB*(PNJ;$|%ZsgVdc zKgd$H90ZhBq#W1WBya!J>>+`YiaeUx)!;>5P3rR>H@M)$&jp7!&7lK(ZY_ZxS(@$y zsR?;jJ%@oP<_wyz?w-@nM}gIedE&2ZPRSbtBRI)-OB`W}Whk$ph6P(@78hJXrV4BE zUnCcRuIHo!?RXr`mg(gN$<0wE-*R@gR-0(0zoO?-r-~d}pS+S(D#t!^6(JTqkmv8$ zq-9U%CSakqECb$S?spedu3cOwMJjB!C%1|rTREFNy;;w3L%r(l0o=5h74Zl7(tK~^#!MOLgQ?P;4xb@AAB58e1{eqq>-`|k3UbJ-TC+PH`Ng0ShR<(+I#bz zZ>mu&b#Lt|Hx=q=2!}_RqXsdvqMM{D8dPyR7V&|wE`I`nhmDARAw-9T&jr<5j}|mZ zrtMz79@?(PXq%&%Q6N(oufCaTMJI(`udv4eqdhg~tW)cV$Y>&Tvt;yh^Y#m>6++|R zLP)Z0v#wiu@Z_ng)kXISgBb8)1wQIO<~;)HE~LB<5ft2Teac5LTEj6X6}>8i0cKCY zqAJ_Pt*_vPe$Y1D<<I$|bc!7bL}4 zWYQ9~Y?(8$iU{@&%~0HE9BpHFSl_!g0H?0KTtMLko7v`Et3o! z5z_UB+u>o#(h7U+f9@+R0!R-3ArEG0p2 zDAzgk)rD@%-cvc87?)gX`z#m<-Lyz7n}f7#7Kp44fPMC%?)tNY%Zn}%STRoAkISs- zxp$-)7V^7I%+Aj&tFh9ACcTI`T8^Q=En(e;3unBY;D`F`=+m6>1{B_Ic7yG^bsH1Q z>Lr_gCjon@yL?EIa@D9n0T%Ku4Cvp|>g~5Ph_5zwkk6ne^}9FynkEb-VMOwRnooLc zc=I*#6c_p8chj3Yr`OL?FYlg~y1|ci_%C(EQR^b%&AF5@jxVU!e@HSvuznDH3$iV%rCwQ;1d7@Ro6Weo;H>M2A8$k4XJV9e$#=BPzfh@NH$OG8Q4 z$N^9x$u3Wf0wMRF_zruJA(0gcIiwLr)r06cRC&9Hg__`ChoZ=3IbMLz4(&*zl>I_R zW9JFrMNp=8wJ0wE6?OTOAX0Yl%d?Z1VD}h7o)5ogEL%aB)}+~U*t-rpW?NAQG`g@G zL-O5?n?P{5>36aSd`K$wtA9&jnZ*@Q`&i!AP|&!OXYXRmq8FT-UI0aWC>#1Cs>NZv zCvQ4pT@&f9vzZ6Z)&Z5eK8VHO9r@!F4*QfdhwL3gUWvecV7=bTv)pHexvm8| zM|z7=Io&3z1}wc3{P6x#SV#NkQQvR39Un(KQL#0Zv2kq3U#;Y06{KwS0<5PK4QCmT zrSS%-N`s5vr98PP6;=nS<yZgE(+Qz*S0TNFfN|ALlx_{jm!mEe1H(}xtd)G} zT1rr_rRI?eRR1(moodi6E41j}+DY6Mjy;21az{^8Zd_AQWiCI%1bu(RLy`pm(x)6v zkXND;73V#j32$_^V4lo4qP=m_M-{xpqfe3}ed zGTtUcU28*Xj-@rV$dXj1*%pFUth~3qkri;=N1;)L_LjA9tm%ABPs*W8qKlBEB1Gy* z{vTWK9?$gu|NlR?u`%ZwS>`M@l^iB#P31I(ZFG<%B-=|e%Bj@Qam}1dOByz((yIeS zrRbGYHD;ueyyTd?q(WX&y?>AOdcQuO&-Zfq{ozuVON(u`J#UZu{dT+FJV>ILl*n2` zthU(hV+QE|hDOz+1!w&G^j(C7h$_wL%Gm`3}tiK3HTo<^B7c1mW`7Tt~nrb{$GiNH;6c>kmrV(Qr zI<)_aMo=~%07DmE;OJh1KCOg(8}-UX!JvhnJCTF^mZ5<+^mj3TZWV6e<OP&skwep=?u1Igqcz)Y4 z*L}NL7%^0#G`O0xQI7$y9K|U3esB2Uj+$umI94Z65q=BMUV&7B+>%*( zF29+Rlwh;L+y5j?meyF7=Dj1eDiw8lubj_O! zPucR61BY(wUbFn;=4^n=HBOxCL6PggtET&zRRKNpzDrRxX2f|r?7GwfgASC z=`f3D1Y5V|UT9e7skGfJ$m;TR?sUM;XX7664&6+jOQ#AB=;P-rgi=^BB?U z1+xEosGN_7Yb^;or)a!FtjRNOUq<>lnb`qYC4SKhfzSk_MwwbY*ucb}en#_(yIA45 z_%Fi{2s>GgBWT`{-4BvLq3HXFP?x~9p8pKS(Z9aWFNzBIegW`JWVfvs-)x>2p#If^ zvOnn|k@V_XFF7EF{~5%R$+YIAs^=vcY`rEz-?4P51pO+!q=1|D!Q~(b2(Ug?aVWWr1##RrG~wCjQ&!|I@Pb?;Z2@@rs6StpTG&wTILE7Zu$dvJwp zINO~XDL91{D3-ytLv%KFYV(E;6$_HwNRdK)9iTV>eSDx5OLG@}=1eeQ52pe-HF zg9JUGcF=P#-!GUOo(`I5p_o*3C?U=txQ0Qt0ZJHwhIrZ z`spor|E^;nSXDCOIhxB43(YO3&TTZ;dXRPOy=l$5*vV4Y1?yc}vEHrFMxTo&_Io;L zCbUP-nS*hj*pARNW+(whS&vu|X|fz73MfZg5-1=jJLN?&6oSCViAv>e$YKzL3sDZF!0kQ_F9*IZW1BbK^wng_Kgqp(-P(oOvsh8L z_udyg%=lvDXz6^lnzY8{{#vbbSD0EU>T0%L0;L^#1(!$_iaj?oX_3I9VPIc=*l?QS z93Xe#xuCDfeM`R^9SvC$Jfguq275sIk& zqoxg9?=6B}i!Fv;P1FCHD5xL_lFV<7a`e`jrbb=EOqyPZ?pQB|#n)_Y_552}^eF}R zBEvda8-o#8Y#(ssD>(UUi~VopQVhGFf+kE6et=|azc!ufC-ly6tL}G2=|;mu@yWyV zRVEA7UspTd0J~gcH0!=8+rM_x-YFhI;SFhrrq6yAvE^-E-OQsWs<6^VLE_5iM`=s7 z(_k=OORPr81!(C`TnSF%dj{{t9h$f#IU=#30i(XHa1>KK>QLcI8e!iwdMIxn5|O8} zxe-CU51Ns@6{|D>1tpfk7w750$!SlcrX4U?yoR3iXH#fATQ7oytF0 z;*YJk#ssZk&N&flQYBg@E5pJPDAvV&zhQ|Fx2cIqYIEMAIRzYt>)y4!-n8wnX!t=( zRf;?It+9rL@4vpYi?AxbI%q+l_)OhV8tZbl>SeNtIzA+`IDxcs?d1m(*~Ii#x2D@1 zJG}-mEc~FuX>BoHB|G)^33!@o-P{Ie@FSIlMlRvWUree1p0&Or2tFbQSr-{9MqMR2(RehNzWeJAiWelNb`TEA9HPNPGprQhPvrQ*ON zb$n-P?K@Z{GQ)#&L~%qhjWtjjQH+yj^5IY3I=B!5jlWg}o!vHRFI=HsE<07^X0q*t z15SOrzC5rs-(-?6P2Z8I=_haDN~hQ;X9p`cc10^`Y_)uz#%%L&sQHxtNYmI$kR7}? ze6xcX&d1p)^=FLQgMz{z!g=D!J^IPLKCG8|1;Zc`w$_{1KJC*vBgQUOi_V|`buH5? zS%Bl&ke2R`fLOG@F>$ncnxkMmHSN71*!(4Bh7yl1scYyad&q47q~a5*!jX#ER(CDQ z1HhL=96%`Jdh+?!u$Cz=f514(i(Y5d9jq{s9}B8cPwAbyVCF2HX=g&C6Ck{|d_GfbONoh>|b;d_eV`qGkLTY-}1G)+OfKI&UOA$rulPYO1tyE6;S)CN& zXc9d!IL1w^Rrm;I^pn&6k8?Z3+w(LU4lH>Kl%TQE?8h02pwuTQFjrufESLJ?MD9L0 z7%)~T^o(hRfGB>UuU;L-Ak3;&dvpT$gl=V36rA6N4edKHir_d!PI4ed;+}gp)BK&; zA9t`}t;)yznK4)IAu_=;iIuS8PSu_Aib1;D=oPp$8D$4P$txWTX9*FV?JZ z(@A_sOfFw>-#EzrA$u}VPgS;U+!3Bo{355dW6)lN7EJP??k}OH@5#&bycN%Vkkr}j z7q?tjb+_ksPZ>3Uc-z>1EI7t71jvA$l#Nla#7wS4PX6?)b|$Yd^mS5q?tth5 z+_L7iN#FTuZoaRQY^lu!0OyPLBNto&n8A?Q)YGE35PmiE%KcWWye&Vki?CH?Z9TOA z?adC6u^aTW^s|75^5)}-klv|Mw>f?hPsLq-9f0G=5FJ5BMkejPvH9S_55&ir5HgiMlH2Xi@l@-+v*x)ip%eSa$)94yan$(6QYf1HJz_<9G?X#PX{EYEm|s1VW7*O#Ww*sq@cD|6G#~ z)w2)G0RHE0{?}=s=q3WtYn}M!z;Z#A|F;(UexP1{sY!6(|52~*h!q(OMg`m8%9>>1 z-C_DTE->y;g>?@=;%)wKi_Wi-NDM>^=gt9ihNh~OD>L;(_MZq2#OF%lQJ9|mezvYO zpSap1>|Emwc?dso!L^}=_I6A|UvB|bG}Go?fy`xf{ygHqfm2YG}~+Cb(6 z;P5TgDMYE_s6c>TcYj}-(~B{)u#pXyksUUZv-J*F97C2HhZaX;Fk-k5|Ptpi-CNq;JZ5fNI;slHV0=>YWxt8iBDRP5Hq z!1jI|RRYE4dLy1~E$KctZLh5hnnT?UW082!4L%pzNYH*Ti0(e0L;BVu>O`?`FL`JQOgjY0x*7&WUYiJo!e8b$PG*Bytq<=Q@oH zbH6$uTrEGlohe+XcGs;^D!jN8G+mVj;l4Nq<#)79l)lbv5hP*9xeFr~t=|P-J6X{M zWlxhre%Bsk)9+aB<(1SEy_|2b{M+kc@%Jk?J4r`1!>SC>aNfLp2YjER%T#R!>8diD zn^>_F0YveO?I={qe7TOztlyVU@q~vv(cpE7=!B%a)GJ+CLe*8Ke8+eNfuiGBoI5^f zs)XsB1X5w~@{k{d5N*#&Fr1~eErpc42-R{bN*_wsA|t$vu_BccMmq+Q#~jeh;Er$s z{?0erk1V-qG>;i=#R7Vbc7#jyqy5?1Sj`RCR*Zbd(KF4&TZ2)^@q zK)iprYd?+;AHDNCPI6=MtF-(O-UsEDCkfKe$^=w8()uETuu;g7F-q zUx3bDg9?#;bUnq^@0+hsPtnV7VWSwh%}Wj%_X%iBhYn!sKlUkErPUxTb*1mCxK}uIAf+cmN3Tx)tY-S` z*1WK=KHp3J$_$;8_OqmG>-;he7OL$kLjnn_I^U-;yc`DHVc~JjJiK_THus@DE}Wn5 zAXf;ED2}&Ai>0w#)_u)g&lA&X2p@PmJ{qxa0>V>yRQc{NW0Tpz)YX&PLO;tR2r2u< z`bSvnWAKu*H*`S+%By|cBr-hR7>{y^R#Nl5u~glX*n4y+(^-1^GZ>azI7!1w-0{Y- zo(_T>%`@`?g<&k#thn!Q-{QNrawbL*jSkgqI-puR-7+EH?>kKNjp`*A>8Wq28R|uT z_99OBYdS4?{w_UCCEhoIGhjw;3_7wMNP+nj7T%F$StO0uN z194G#?tf~ywK4iGPoGC77dagCX9AlrFnk_9y6%>T7X)>43U=H^@BsWU?gBr9^J(_!t$zI3w~#q zS!TrkgBZab;68&w5=C9|n4$_p|9VuS)KQwSWo6wQN!@KQcSv~N$~>%CKR9Mn(&uVC z7UN~?d$>I{DDAn2pSd4{_kjyT95z6$%QTOFxDb>(adb@Gk)O9Kt9e5>Gp>dc*Hi^B zbq`mEtERqq5^zS|6V6@XnR)?mDtpMRR@0OU7>AJHP0F~syZA=vLe$mWqwysp>DlK_ zKs+YZ*Z&>?kS%)t)j1qIfJ3d8O~CL136xMbM>Q&`CoET#XP*FVwU|1viIpvAW&6Zm zqn~J5S#x4pM)CyP(5>@ZUs+l9qka)aIT+kXc0)8+Rc@yBfO#DljzFEqX3Xb>mp*cW zrbz7|oB?7S|7T(*qnR1$C--F(*);SOeFI}O)9sBy>kYM#BZvF6uH>Ky@^Rm~&$Zyv z;5-IqYvI2A89xL-1iS!w*>pkogEMZp_XW~105-w)@h`>aT2>|X*JQ!!K_*D}EWdtEU_DpW5H=uHMXG-YmDOaa` zCf3_M)62+zwucNITOc0X6TJ5fEoCRTng)0z6OY-hD*V}ldARfE0_2_R&F7Y@jS$~v zfnafj@3E4)w^@S|r*H~ z{9K~H0|zQzjQwYbM*uKwh6?22IVkUm*0s3ImITu2nB`RI)irZIh z1ujmb4k7t|%`V;*PgET=*I2HMUJzD-(eD9!AfZ+33L=KivvtHM;s=l_+npH#E4j@4 zpjHqhKM~*Fc=hv-r(x?YNMk+TB&6Gv=cqdlC)v}KIoPs5f|yF+ zM1xMd&m@>@q4MidTY|CPg0d8kU|~W3+n)RW*qYiTT$**eBrxj3PCp~x0QnJUxm^=; zf!Y?A;`(Cefy(@gChep2HP^_4Y* zK-FIcWxI9b`v-LW=f=*Xz(QEa&e2cJpMfPC+Xl3~X*R3fv4ey)EkcC+2Rdv+y4)Su z>J*0K(7;D7eA7l6i%n4SI@0$lAUW6)=2WsP^w-@0SwWcT8bDbe`9VZZkJw5>2Un)zZ4R2JsPfk9kNfo z;&dwTGu8H%M)TxAscVc`6STXFBtmKKw73_mxpz12sBksJ!qMOqJcx&Zvl!w7-d?wA zBA*DAo|akFZqsAin26_KN(1jO;TEISbW=cm(JF&Ej*Yhm(DxmJ)zKmQ`PRVta2P8z zHX}9&RY65nLaT1I8u73;2GY{4l1mZ}$WUVJ zRosQDO8pHXy=^AAICoFW$8#h#?r?&ncaGZrR5=k0a1*_zJQBof41wWRNsPw|y9jmx zRNud8`$68MC?&l~ujrRIdhUgrdKZrIddoJ==bO}5F=?G|$NWWxJhf8X2v;Y<7LAW0mPXH?bQv^;r1GY0NuhghM z?R$NI)m3u!Z8W~r8}q$4(u^Tc2tYd663$YrT?_F7q{69G>KI~3C`aYaai9~MJ>{e+ z`Hx(~d3M=Xe%QN>0arV%q48nu8QFyh9P#dJYz{ zq@{^Y==7IOrmqz?5s25W-N*oV3=JC|Gcd*q1RFd)7Bv&EDIds5XodV6IJ>)21EEJ) z5CjZQUJ$I?DWAUFDBRF3+OB4l-;FtWDal^2&1}xg#m3LVDJZBQ^XWuEXE!=pX@wW( zt*Q#q#&lQ>BN_V-1OwAhJ4$g9_!hjNy%~Q7=UCtR9*QsWaz{@D z?Yq2#R>$UsapW8|11f~uW`fGngmW~-3qU(+SvU1_d1Zo?fQnqP?=SFE_(-O0?vZ?- zCjZaigWF!0aR24~r~*wF1tI*a2#`Y%)tK1OCjp4tKGKT~XawAW*qjustzN<3Nt*T; zU)us2b{Cmsp4ACWJg^l)Uv1@Xq~vL}$@{rvVZkROe&kJ`8Z)2oTDJnU43u+#9Rdy@ zr`9gO*OqSp;G`s_pxAHO^lff>_1i|*o9Tkn7W)LYk`sPsJ3r2dt?TU$#;DQ&1)+)R zJXBZr_Sv*oNTLy2-qP<61f7p^WQ(B=klunM%stEG(FAtcaCDODJxs4M)#hCkyg^| z-rBCZ>@zVICV_9Yx5Za!Q5u_x|ACyp(n%|%eXc1#XY^PgTC7-Ab#!#fXX4Rb(u$sd zgE?PCgY!U_s7AVGekzv%n%HYZGySE8pPx~0-d_zJx@fOky&Z~YQE!Ab+odL*$HRSj z&+#3)t)Al?xh+{m>lZGR zKNG+=Z!BTrQzH*muL&Vcl=>ESF}MwmXiKL0b&`YdwSmJ#`pTYwqd)5!^9R^#jPjwt zU1J~dZ^37^4`*XK_25A=Wd_hULB|c}beF-R;JMcr4Nh}0U-fn(s`yquTi$vn{7W?4 z+vL?n=5v08apdHb4=XM*W`Fv?CCt7K?U5qYQ*TACSAr#{G`Vr{jGMR0c=JD|Uj@jq zBZvtD7n@RxsSV(*Fk0N>1*j_@LF>pdsz)Y}z;fLS-wpa{i0aMD)CJL- zpDzqn+KZ4}TD)34WWgG^RPtStKE8ijI*D5)rT>IL;8 zykm_cc<#Qx;p`l)_Jp(<>vR)gQ3VO&px-3+?%jY@HJ%K^fcEIGqG)n%mkmiv{CrDeMN`+Y zcg+h`w4p729u&Of(qE4c}oM{w}4ODHBC3>iN-aY$~a&g0G1M*@8}Nnysk7Y+jC@F8KVBZz%7 zFa_Uf;h;AqB*1E#q+*nhyz)1dhlkaLNu{7;EbwsEC%ez|@G|Q1hzjJL-d}(Yy<8=b zYh5qfe1oey%!Uk38me_ocp=~ffXfty1}HPBz~OehIdJXI2mAs)6k%ll$NH`P%bri$ zuF*iY=vsmqv26Ij8>27gL$GOT5xp8KKxd7+df5}76OU$YE9u$siS>6NZC6K)<#H7) zqVcbhYims|udb?#y^@l=P;nYNS_?5&Z#J(>vVXUqEhB=E1l+8%v@bWr5b-I45-5MTtbfI=v;AMOp&aP z&2nC*foeanZ+n*GC8uW|^NnY&Q+xmdr>K@3bDAIHCf7_xS44YaR0$%#1hQMlBnK&n z@M!@@YIJEd8W}A%Br3~GR;g)F>LwxP=2m&%ZFJk!+XhtV42E+92Z3fxn8JRxlz3e_F8ey2dn*g{f?}#RGUf+%W}EBu%>i4RIx~POI3(iNuic9CotFQz6VRCHRo= zOz0)mjVsh#fR({!`HyYp4?Och>eG=04qs7V3j~C>7ROhXS!@EVd~1s}5%OkAuV+k4(1u*{P1Ry7)S4m_Hk4c~6J$$Jl7+MqG& z60QD<{mPt#x=y&6*5glvPHq>(@MC+6ImrlzcF@%5C1Ck=*RFwxx1l)3_Qath(qZaA zdKU1)Q%NwBPxv?Kx&6GqMTp;tkUEQcf8$_SM1M)(f?Tim&-Y&i$gPUb#o4Lnx5}r| z!@64d`MDjy>s_Us(mv@Gq2mU&8h>HI(IG$$fr06G>THn9uC?9!V?t35UG%;0kRUR) z2CRNAa{=zN)&7uRtD2s*8}Yt%BUbJcGrSQakEn&V<00&{_juL=?iFTz{SmHIyV;=1 zh)p=NqojzK_0Z;Y@cp{6@#?(xFS_kU?6sRq+e<}WNr#<}1l+tEBvhU%&TK{C!l5sU zqA!ok;|dPYLJ|h96c>Yd)0rw=6~k^|TNpd&&<_T>l zz!b)%FQ(z)cAf!BI7ueqrCeZ_!th4s#?35}%F>IB!;7Um&$d*i_@&UL+*^&bMpO2U z!AH(f=?xiP2I%}X^!)C6tIk)f;v*6*xrn$p`7|ob5bq~Cf$}>b^ z1bJt6EukZ{nC`RK3fmk1_V4lp)%ou$kT8?pqYu=Ieq!WLsCuD@jVAFF!w}Via{IPu zCB^QT-Wx1HQCo4^#hqY=<4Ux&RUKD{rbU~p89NA8N;vRlJJsSov)SQKX&;&$pM z)NbxJ%}PiK8`~$5P zGGiMAR*@iXfnfRk8smXc-ub+O0J-=tBgIXG-+$gJ#={u0lCJ;Ra)OPa@ZfzPueW&E zS^o+%a@zkZy}(IJcK`?a}U|~_eBQ9#+eSg z<=RsszkkrD@rIh_`mqz-pxyV2K(;=^*rfQ&T6bf`=HPvIo`kA;UzI=Q1X>Ov?nxqa zlC>+-R+ly87OIJt0^mg+4np<+Ku2s<>ME0-w_V?F1_wg2Nm0<+mZWFCnKVtBY=Ydz z$2Lyd*L=LG%41HmPj4pF`wg|crMY{Ft4GEOsk)>w(2F$Num{{&a0vP zu6B8bZ@ZMQXOj`KdNU&7l?3tR`<%}toS*=u4Mo4s&2N{`U`HiKf_8Oc+TvN;HfDGN z_oSWH>&+}Nv|o*B}R}<%^zYAKF-`` zL~hU*L{7BEKA(=)Z`m&9`$(!st_1(CgV8nLkhk&BdkxM_?;(3vzDzq2d9gALvhgJTFuJ-RQHMTIP^DJd2!MH_H z2a!$o=)js@FbLXp_EaLlg~9eQG|QyDxf&1J|J>z{jCZ=2Pr7kwFg#|3OLQlo|4g~# zP4lNoirJ}WJ-@jv)x}17ktd1wgnrp22>tX2L*HSg&wXDilWybU)+z!oV3EJ*Bc6&?5N2H1JzZ z!yo9Znex4CD>^kh6%zIrjR%)ol6A)0)_UY)V8yq+u`NFs&s-x1A7A%IcesX`m%mZ7 zL$g|;o!;fFm;9Uq9W6D;0g|;t1p=UB`d}gtbN*n5AT}gBwP1H7z>Ws*)z zIv*0b;NQLio|3)qC~m4gwmZVnqna3PfWEih%RMX<41=L20~e4PK76=R>y3m4m`aiJ z<=AO=!!TqtjKyJ~oHPA8TnGsyjqHLWw9FEB?D{?`_mBXZtOsUTJe=_P!I3~VQoGw0 z^Jg-PR@Drk`c;6i-A=4)s)DF@P4m~KbjO}LpTh;?x_PH?NCfV6z{PDj=7+S_vKjJp=_bn=xX7k6mmhK1TG`hH~TiSJ`0 z*H|RjFl}E`vZ!5`5e-AZ|LHUY$hV;j#DHk1JQI9QU2->h^dZZs);iXR2<7hAW%|;M zmdk_!InkJO6vxk+3^EJ^mYplspD4CM_vPp2aH;j@e1DEQauNb(L7hqG%9sd5|4DX+ zPh{V_%{(Ll6w}MMjEd}8KH#I0BU>NvVl=C*d@r2@#N9=A=LAaj$KKD~7%1vO?HZti z7X;bl5FmrS!QF#e4S{~s8M+`433#&N2o*5Cj1snb#vuow9qmu8{55e~&{k5R0D%Fo zMHL7N&tE^NO#3Rr1t37f;6%ajp@|HX2m`^LBwq7EV<5eD+-1hSm*4Qtg92e*AAiyN z!GQ;A71k^zW4@H*(tWvkO-$fo+C9@(`P5}f%k#|MQ><;N0WCX#Zt^C^0@JiP*1FQ5 zH&i|*B3tAUk(}M(hllTOz>3;gzPr-zPHNj)Ai}^rGJu{GtQ0Xt*46;yAxtb3P9PVL zEmU$sD{Eo#mIbfVUnN)Hob|nQLSV5)!HLvdy;0iUtX=KM1KVjEbz>f3E9r3Srdo`A zTiDnfW#Gu1Z1J=Td)Fwd;5*BQ6p~f0GUI*o0`G+d9xT-1HzzI}v^5>X^>E;0$=c3A zN!#W;V}hzzEk<2!UF@Pj-rBK#3H-$F)EtPA4Dwxc*T!2BjQ>w|un3=)rfcWaYS~ z$!H-$UZdAwOYKh!KQ}hFs2fC@GkK5#UaTF(q7|qrlOQ{Eh1{%`zkTZnYRtGoa z6>uSU1D8$%nL4QOdHe$m&|(W9Pf1|LMg9LDWdPoOfw01X}?b zia=Qkg>*`)E*U8F%D1HUse%YI3#;$+AxIexnSG5ljzX6*%co~9R=0BC60A&~$ z0sK`mv;QwcQ~#7-MLlFslcLQ}fOR7cti-_W|Mfgo38!d%v_rsv5A>{AK65oKQi(LzBaT`lK}Ifz|3R9mQZBv-w2fEPX6A`prY@EoZ+)!Vzf z7P9Z5pm>1QU2Ws-!-rJtke5rq2wk)C^mDmGH3zX-8TnUI`<>Fv>pE%;Ix6JXB!-^4 zGIlAEZBjxuuq7D+FD@utxvw_o+Nm&**wS2J7{+DUH{6E68H-r}#OOPIaloa=5IQADY;bng_PSMIVif$BO1ueIY6rngt1o=p^b z1hV*H`|yKr0$2Rzp&b;6SiIl+?qtM8-r#BAkaSw#C(hk(J}$v&0e`wof8HP~CJbgr?n zv1v(pG-#bvXkxhqIgeuMRzrKU{F(j)6f+Mf(;`$1O@Y*bLj#}-)*CQ06dn{G*AU|0 z)`Z771*n27mTSnx@l&n^r6vp7-vZw}dJsoA+2WEcl-s%Q=ze7O5UXw?XZX3AEj|PY z7Og24yozaR;NLh{tM&F*_wS=g+s-pqt(9IiUD9X%2l`f(`|N3Z-{)Yhn{sFNS5Fja zv<0tesXVLw?FW;_@i%yJa@QONp7;J}@n*Abs%MGQ?_}swQavP z9oS+0#J~(hRZng%BC|I-k0ROn{5>L%HQbWAhxIO3dXPk%Z6q3>6L;@>FF zZx-(|{Q!~M8w!w52^)k|xQVvvb@tCT*|G`_8xxNLyDawS)|&VS(rdH&s7+a+&bhpT zms$S6T$&!+b?k8Uwh`l@vfo&lpjt^{DNuUX)1czBb6nTYGJ$NGyj(1iA7KSNM-Dg7 zQP16M8`lr^-cQ?Dg&JZ*9-mSZwzbgz(_p8|Zq; z<9hfY3uWWw2c$wuS;(Ri`BbM z*y5BE!LLj2GCtOkX7+Y$&JTcYhst<;6?yDc6*k@lE`ib!a1U^5_r3h^gAUrAl{mY- zB*>_Mv$xs#umH@8Xew)*9E7u=1+?Jjrwt;X?+#T_?R+b`GFUHP5Sg;PPkoG52eoI! zA~O+St}|ptUhPF-mIe5EulJz8HPb6DRbmMPj6>_6f93aIf*mxN?o>O6*$^ISK+4Yv@9D@cz$X(!3f_eM~n6eF+H^Mj;Bv=j^eS1?_WX`CcEl^zn~rF%d*?pYL73no^sOM zHw_9tNSWozr*~8gQs|joPQZ>(ygJCaSL?8|-BZTc{vozL)qhbQ|CGz^jXLlY)PuXe zuiTI1pXMZkHWzCbe0M!0f$^Ps`hjuVW`=ADE~WT3{C9@^j61Ypl+>y{l5Y>P?HW&C zYa`3f&HgaoxyxkSt6j834Vb`pexuD%=-^rzj34XCvj^>1n6L%7OeyfdGTxi-0G8|s zVp+~-(gfT1l1{7hAx+$sX~Hx~4O1tBQv8|{fDyniOmQpND8Ug_u;Hx7Jh%ar{B_0% z{{i|iw0`|YaB6D!rA>f<_;t}Qpr%9#Kqi9I{zp{&5*LYOIl%6s_JA=`)WnzvGGYDy{JUYrdgu9y^)ev??jtZt zL6|@KuTbxU+H(#th_As=K`n=Dp7PHZ)JIe9tJjgpos*siWM&4tPUavd|9b~XH&M^F|2S;BML2kya4)B$r zQs})r!9-P_$bJ%4k_;w&+d7`DcS5IS8IDDW>{R$rh=xR(VUxkI_H?5p1wtJp&^_1Y zC^6T(xcde>$Ei=qO$Alt6Qw!tnoX4Z?%%54Htut884cY@MU>!czRsrC*R;7q7N}G! zZ?JkGsDM|Gob+fp7Sm9E7fcxpS_oS@5BC;;4$J)CfS#~fM2=Xlt7!G;b&ow_G?cPVW!qL ze4t@2pv-g$taOsf@q6AT;Wf5BEFPqJpy59dx;*}7d*0PKTgd<5#<}`a{>&R5=2;j1 zygCN19Xc5fCNQ#^bC~pie|*~eAjAM$z4|>fYe2(dmMQ6UFbHzW*)b<&>mb{FlPdkj z4Q*j6D`RyT`M$lDx=n;Lhi^Q;8oV~MFK9=E)u?TBBu!h0W^(lkRr+pu4C9y5+}*!k zT$L9Mf3Gx1vsV41Hqh=M??n{hXx~#%4oBB`(~Qi2YH+C4)k7&`4`%HK(l8*Oa<4PC z8aGy}qKL}*-DIdd0n0rVTDYcRjy`*6hTfI2HF81IujhLsG%JzF|GsWzH}jx}!Rp)H zwmX!cyF*@x(Q)Rcl#=o<)kvo}!m&D-qP1!xG4?q50#OjfHn6szA{8zkGOZxw&yX<) zk~&t$nX`w;FD#@$`1g4D20LfHy2pXV#R+unmQMSx!dDGg49suGO9!_Ao0WfW%inrB zN3bpJ$q|#$QV_^O?HwuJPV%>` zQ+N9+l3UGQ`%PN7_fJ09hkd}ym6$_1STTOTE*^gHQbW1+LRDsG34Si}BjA*&1&6GB z!#VME?2ynp|3x(XXhhQ%yR{2M#oH44?m5uqENB)x85NlXY--DY9s+`iHa^s|C}!e9 zdoa?G?~!a;p!svb&;S6y;Pbcy5_W>;;*K?D+mK*5+Hoz$4%{IjluJ_Vbk^V`xl!EL z{`pA+YOPoQP?MIQP6y}cAHDn-=W2ms^03rO*FhwIn(PDyt>z~YnV=G`oNsMyLsHov zGjje^wppSZ@IEDpy70~AxgfF9JJqVDqxw+(q#k|;i+d#uFxADN+sx8Z9tp0RG#dg<)2!o%t zp?aGfFB#iol+z5Reb?On!sWzGi6OHDA0LiyD!9=1&t^a7p=k1+#HxmN5a2@Y`?=cp z`mwJr?xZz|_I`EqnHeEtd`Rz|+Y-zqsiNjVrA3?zVZ^0YdZpoN*f8jWsOySjZROrq zKxvvWdDP{H(GWKRDg5bL*FnC4kcyh8NhjXEg(>DZ&J$0pEapR0FNUQj8KFT^%AEGS@?*!h-gMom3);;H@YGwjIvqEl835XQ z@ycdF#I0ztoe7wEM61Vs6=B$F!5*RUc-VY_gyq)`9IgeNY-ilp%6)evh-xeEPF#-| z+Q8(D3N-Qd)6%lyYS1a^;0eFt%gs=$cV(lkz>))Ii5C$A;8YI=XtR^87FT4F35u-{ zyzGjgIvv&I<^Y^(P%6FE*3Vt$0x2KjsjP?0ThU2RU|(( zeYZ}}Dbuc9dvs(@pcEONZ@b#N(bLulKwuzduy2BgOR@lgs1kKKN(18DAzdd~Z-Zh! zU(E$yjs+zyxtC0$E^4q400 zG8*aX{bzEE!I7Yo4gr)ct3T&G=xhGWfexFElFBg(AySQ2Oc59&%d}EhH0;ELn?JHl zf7;xy0Q$sWTm;M8_3%8|BG-%J`e1wRFUO{w??^vD#nU8^W=RD$3gQm zkH9$7_p=VF3(uiRAjkj5+3mJm!SU$oC24r}{a{k1W!cr$Cx1zw^}p~8vR#c#mRb81 zeD=++hm`y)ekULPA8qtYJ5k+05e~27m#F%6WAGn-jc!hhtQa$k3BITyuwVi$s#B-@ z-AF5Xcd}=x-`kt;pbd3wUihuI85o@n%F^Af{zm}*R|vwYhJ>~{^8!T_IoM4?^_6At z{&&^%Yp&G(o-q&JKXS(p(Wi2M^!Wcf4Ei@r2c|;kEk0hLjy?Iua5XF7;2*u*2cLnX ziV7k;9evO@*$7HTR{-j5uNQ+Ml*p}IZ7}6+H9R6rPsX%)dk{!vIT>8wM^Fx<5JoP0 zbHa!qhmFFF5L6KAhg*oEKD9zdX^NPD(sc*a{rat|R*dz%n&2Pr=gq?`EbDbvz9G)m z5~Xl03!9VTpsD`rPvcIERN?X{Rx6&wy+~7~=LZ^^rG?^nf1ZT!(=&b@?+I4k*<>i` z?X>T)RO+c&X#s+RbO@~K)Lc21DR$blQN`iHxHC;d&N$;pZ-h#Rag&dF`#b-r=22WP z3=a|z zCMe}l@yqhyR%>Qp%`iw_%7gHpDLqKP+*Vzc-M2(_Z@&4xL`;@(%`!8UiL=lc! zxF9rpDKgMmcWISt;pMr(XL-q$A>xTUt!B}-;r;6$+|?^+=HpK=(%oo%UxFHtTtGKe zH^g?-e6+E!+IB39fBnzt^K9d`m#sUqCr|@rjgL=;IdqxtI<`u;8=QuzKK~sR7SvtI zGC47mM`)CTNb%(r3fwW~(;A8sP&0Kx({{C2F4&CSq3Tz{tWhNk_E z&&3Uy0HeNP7k5{!cb*#jq`m>7g0Q`U0X@cETEoUvq7{&rIhIZmWZLYv)`(us^;hM# z%+e?^w-)T%$-)~?LCVAlxRrK#y{l>t^+A^F0xC=^;Ifp5JFMKUWh6o#BAya z3gH*tz}(j0JTD0sXFp}H*lBmPamqIoxB52YU|z6$7n4ydxwmFw#a zo2~U9|IPvxyAP=2ktLUF{y(P9J&@@={{NqCuFKujFn6)(W-JkElH1&NK_zvBv^gc0 z$}QTgR7)CGu@=iV*L0;grF3zt#+u8KU_V*!m;AuPP=g9^XmnXZ5R!?EcmGM{j0jWvCWvVV`=c~4t?{I{9;%SKkOanKIYEa=B2|8Ov@Qc=x zm5^G!g^^8d7peb3Q)pZHKEz=l3U5?d+_aG399PAbI=DWdw~mK2cghRYW?S^h{x)S7 z0@A+HE>>8vrKSd6gcHnfjYF4xU-O}uTiPw5TN=kWN1nmM$p~T@#<(OqvcVxlyL1$s zQjOjls*HQzQYLO0q1^@rK%!bN+b3W(DZg@_g#fE05=xuzj-J0_P|T&nH)BE1;-`=RHq%QJlhiofcUSde+$Yp%x*-xI6yn-EsxO>2oJ8{k!!t`_GeFwp(pwy1SIOe@Cuov_=K#cAtj z?BY$R^{xdbk|-9v)otI!Yy0-D5*=+ca%NmrZ`& z_8Z7kAtK?Iv#BefI1PFjUkQiO_4L3P_X1OSJFf8)p+8M7gU|FEY;l#EIHWj9Hz?c< zBO|_QXfR+AX%ReFPR(EIHa8J^_=;wrr%}rTlS)f0s!0SlVEqBlUf(6 z@<`|=n31#PyoC~`7WI=@9C1WIRzN6QTU{{Spo5DvFq>$7$JQ*wU1&0n&XS(a%A%njlo7 zx5koY1#71Z4+9@<=|9kJd#dKcN8|qWyKW=8$j4W-3YVE*$W195Vycbus1ZxdRv=0( zQInQ7c{sx82TCR*E`riAU?%ls~*L=eh@5j9IhbLlYgKuay;3z~#KnD1(+a4;T`DM-3#eh+vo zXfgnZtdi3xS$)~9nJqKk3bVig)dh-NIlvJ?3Yh<}XM(7s!3%O09Fdyl1x)yDoe$a+ zdF+t9P&1AW54Bmhq3;|4%yukaUdF{EnqdSOo0P&(_ea+j<-o# z7eLlubPnX$YIsX#q(aHR)P~Xxq;!14d+5wmCm106{*wiHKzm%8CRSozdNn|1j+m~1 zpYzvZ?wQ0~V)pEB>nZa06vlQ`yT}wy5qB#S22)DCvgZOKr36KmJnw`FR#Lk3a0Kw_ z(snR~3WNf&9rB<6YjkRKFo=5y)0MF4C!YG~GlK=@W$&eREo@17Ht?gB3zdelmwt^9 z^(mB;mg=b00Z)0kC>GbngQ>S}qO@3Y+TS}#f7i}s2U{O64u!J1gT*7G*W>#tUt9GI zRY&r}&0H4B^hOLIqw!|0ssOi%*WKEfC^cFD%R%5x2`v;N1Kjbwm2obDMThEuZG3ONGrb|@5Wr@-UW(M zenc8Sd^5uZ@g+z%bk$s*W>2Xhs~wl|`L|{+>grqPb{|s%nT1@m&CRNNz1g4)Qrvbg z?q+U&eqY(wVCOSOtmA+x*7sDL**Fu5rrNG@z#YBQv_sdkarpr;4IYQucL%GWEeO^Z z&!M^f$h$ghoqV8q7sp(`_qo@#vL#TmZDMmk4a~AG*~cZ4zqcx}ZA=eXGkO&PR$9fH z0E=V$@-=tY@SqxMJa}V9x|TPm4RN#MAEKa-+I<}QZ&ZKuqjm;oAN3)8Rx-cw^gaV2yn=7x;k!mTepu%V4~5IPvt`gWSP!Emu~-A zZK`!Gjp}m~l}~!r`&1)-$H(?D^`eINu$B2zE{K;Ns)(>Db}n+Jg`c=w2S`N8S+-@M zX_JL(pnP{KZA(D!@l>v+Nqvw9iP6s=N>_H} zq;^k4+{*|((52*eBi1YC`tP_M_=zTHcgGD0rJddk?#gDHH;|E0+i00#~ubbYL zx#S1yCAACZ+he{sd%XciZv7^Oa}qnxUQk(M1LdPG#ka?>UrhDucI*lKYxT|~3_PVx zxa<{LJlLsy!7Z2T;T#8K3r(d$^Jz>c>kSEXu`BKpdn<+#iRG%s2Ldz$=&(Q%-3c`; zT?v_@rn-1-E4QDtE(R&)IBa`N<*wMHT1tVTxA_o$+FuZRDyQbVjKqPeqlRPK7wEY` z+ZQ(&Uc9pYkk)f>8F2p^L>(1y{h4*#MA-s1ZMjo_(~n@sndd303-Z&+$h2ZzG|*sRelbH=txv*EDR7h?>&Kc-wA+JU_%#xdHz}(JWizNv(0b z@>g2-is%&S2KpKnQDY9kumDmGln@H2=heh|m_r1pgc2~P+fx!Wj6$J>s(06>%Fsc^ zI}{QH^2PKN0t_Etbv;6K92ld~t%h`Xgq;)NGPt8{>M99KlPT0_)UGJnkZ_-N zoRfl5$>XyAM7=a^A>8Xa45Fv>a`B@*IkjsSCit8HVGbScUx$(BFs@|b7UzW#1{-tK zo#T{qed^P&0CQ^GRae3W7!1j>@g9zoD;9myqIxaP!N9G~4eP2Zo*=ErsZnun zE#GT-qhf~q49bn8!zN^up0h$NFF*?hanK6@PmWSUNWs0TB}Q?LRs3s$+NR8Wf@oB~ zlxaV}M6?Z3b9^L?%H>jo{xBc%8)i3_?*+`X(bBJ+6Ek&cWOEP8Y}$=fIpj>6$>l(9 zzW!w5==t>buy`NerJfTr(=_>*s}c@6@aCp(XElI4_ge=VYGpASd(@qnZ#wim-y=-dJXZ6*Ic}Jc< zsxqBnQ(RVA4@}Bg=Zu9T)SQ^xJ8vcA#QW>IBx|Vt77Eq=#g7tB4p&WK!=WfHu z%s?kr%%Mm&xiCu}xSpU{?-mMiHE?-%x+`pIUlE|4@Ery7lOZ1N`!^@lItt;LTR?LQ z(`P(5*80t?wWSR1+UDap9&-M!@L=e3o~`KyS|~;RX8Ak{n(3){KKCa4Q2Uc_5$+5S%PuS#ynabwKn5cLXJBdTCR#vgYX%A0AIbV(FF9Csi{iv!iv@*- zMlXEyFb+3*59^}2y!07ELKpQ6vm2}n0*Q=zORx^`SZ7?jLOS8u1OEM?!IlrsUCx?` zBnuSu510(qM71zTbO18DB-8c$2*Z1Ty_v-!F={hO(e&3`x@7l49!w2Ama{pZk%QHQPz#k(PRcW1S9~e8ZFCeWuL>te8I70Q z;a=0Bxsd!9yPGdYd`4Yb0krj0c@}M;@EtM`3%)~fqPX(=58ZukdT?9RPUm`f=(Rw?7ZtuJ*F28(xqcWfG@s>h5N+KC#~0JiZeAXg`NYKJg+k zDwp#By%7O^5YvW5*}rc={GpY`3Ex~ya+H*CweG99H9|!TyC(Vg^NW?Aa98h>l_I`N zJM|<6=TEp@{nEAgQDu02Sx(X80#$M=j2Avs*0jYl`jHqhvPd3W(4)REYMkTTjEi4p z;zkLbsUa)gN~3USOEHKg7b0LCgQKjMIR{)--7rfb54d7SK}?8$OGT;Ii{?^59eO<; zypEiu&UrXcwJUs6pgy{-6tP;sJ3Q}HWhsdBt_(T(xz5_~#O_8hT{^E$+=w7Pc&cp}0WK_|R~SAWq`Jg=`N5N$$jxPA zD77Q50Gw!;I@Wg;5j+}V6K7B4+`!-+pC2i6kv!5|jJOngiPONep!9XXt!I<51P( zraSpv3sr}pLSd9V5>*d4JreC}zdOBp$7QvZy%{;&-YM(4vlD)J@hr4)S-5CTPjxti z%iHK#A3Hy?H>wpFu{;_02{)IcHEOw_Id*u_PMMTmwfy9D`Dqs3*6Pab{YPtH2iIf% z0oe}sMydN+-!+h*At%&hJODV?=^zdz1q&Z?oLQ0t=!deMB{#cQtP@&nw6j4T)^@;&W z**I0y$?hAt_D3t?$Ab+5H-$e0=AC7`>Ij_IKCMHlH1+a=oKD03rV%?{g}T4I1QvpO|dt*+uL1DV{wh>(J#_Ot9-W5sluXkH^n) zw$h}y@t*v62sH3;ll6_7&_-K^dvcxmyN_it4PI-B37OKDKyj_&pu= zXv-`LvmnXVIaxk>ycIPmIr%J}2+?v4#60OQgV}rL`D?MvRjp(T_%g%J^GA^5*8)m2v+#cSs$;I_}zm zM*1=%#*w?^ZeZ-PAi#`QE-3N0ii*_zIW#G>wc0dv*Yvr2AWkyfgnQk`w9pX%t7N)R zqgiZ9RSX-EAu?pii~iV4t|6CMMfa&%psrDgGQLO|TNTgtMn&e-+*1b6J(q|stqShRMq1xIB!3F1~Xe&-g9$h^3S=OA{`B|U2|&3VvaO<6yiO{ z@t>DYJ@xwC&`?KU44s@{{E#Drh!R#)18 z4Aum}%KVxOriFJ=mOj<2ksZLmBg#2T2fD9&WIwAPJ?~ zbx5eHv@uBiXASM?lcWIL@yu`c$O`q3B&}Bbm2Qv5#=Q!Gd_6BRFB~nawiZ0dVp^ix zL7T^^7x%UKAv}=f%4yDOoFJcuf1~k`dH)2mmj{Zxh55TP1f}r23T) z_I1puaqdU$5@*l#-5z~gu5EEcg@`e(nQ$UHTK`?S_cnFQwe5OB^*Fb&_0;zx*f(AX zcWs>Y2;rCyTjz)9AvK1WE6-Rs>4kjQ64l}oRB<0174OVxjK)BFukJYTWqey=@P!S5 zfArHHJN>kQ3L+bV|F|eXP;E9itq<562h2nfuiyy5zUmzcAu;}$Nyq|Uw7wc0-d4Ev zQd-az)_ad=&kwUlrJO&^)f@>hhqpcr>6M#T^}l=vBdR8R*ya+1Pw(%%wrv?F3RA8? zRupc_VLaj;HZPgI*x>wZ2@5vAfXebPVm#PBFM5uGhHd2fT&e=1b}fKhA=*P--i`wZ zh8t0J^c+d8>_*uER2|?w*nXut>nQqgqIghYY}cM2jAxEzfcv!bTbJJcA0K}4B6`zq zik=%UtXTuOzeC1l2LXHB7No;3CI%CEPWiAe*uM?C4dFAz$%S zM%6S%zx0k&DhBjoI;=EZ&|Qhu_ZKxb5j^cf_HODnn5AP=TbkBB%WxFRe00?`c&3W> z0T-`G&Vp~X+A98@Q^3uE7AE0p_B!Wb$^BX}FDl_(Hg=6-wKi+6 z3Ca3WaWt?hHR>~bdIs$0gCt%DW!3sTi>lXQ9?=*3_qTY7Dcqvv;l*L?4Ss1#a zYs?&xMFR<711e}!JRzk;&eG|kdO~lq`M)`LQBNmiOhQ|?qw7DY??AAEccqOXs<-bz z3ENQ62Y1i66Fzlh`}HR;nyGhnPvF1ZO8i2L&i3ilpA4u~Bu}9Uu=Q);EIveXmtW)q zOauo%va5JY%wqYb_6M=Efwg`f2;}jRjxRy|NtJniA%5mlk#eTVkwF~I_ji+L=q{Y@ z?bz6|YccN|(Eyo)D*+pV-E{GU{35VQ{p&$p39Ugd6|-^HEnJO>uqQt~mfF1@GAqBP z4czpCpqeAiSi~B`$SI?<43W~V$K`LfYR~21dOG(zf_ZEbwU6*i?*>jJQkV>}^UBq` z9sI`B9+B}XT*BVwWI87s(U9`ykWs}o^5E1jaWt$c(k>!KU+Ib{`&|pU#&@MXnai z`qTSbZfzGkxhS$f2z_`*kQK;1;Z+@SAY5Ra8z`~fblD#@{TzT%}838L*!?d zuiapux-X}ow8(Y5R|L`&_R;Ewh0f8Uye)$loT^sH5vN*uD$Os9`z>1@fB9#mreqhT zCh~4*1xjM8@~r#m;lr&KV?Ci$-eq3Edx!qT} z<80?mkh%?1<#6_#++Dj))D&cf|@$&<)8fcabni9V@X>r-l6i+|BCVUX~x21{B#m<_7mPOh`_d{@u(zQ9y!27 z4FNwsCK^5k{_`mMTQiR#-6UuqPKrv!e`lXMyu`okBQAQr}0(FDCtAA4E2B z9J?u8R?(_wt>0)aa%V!D&!QDZ06`9vD6J51=zFcvF0_9j;Mo310rkv7ERco=&T#5b z9uu_bxcL&5Y?J_qM{&DR%*}tGT|`|b-qT6aqAy|tO9BoT2kc-4C7=uv)CaEp0MZP2 zkYFQ$o)_s#B_>9FOUvgc3l+*F_RpOoGDT)H$E|4%q+bGVG#GLmUC0b1K)9BpKNty` zG2k)}fq3(RbRF=OLnFo;4V)%@+i`dz(k_*M;$hDWHMhtHk-HWoq6P(2Dxf*R7a$EB z5_7oZG9NmJCJa>tfSBuR&qotrowgw&ouT%7-{qX43ruvRQPa8$b!C)3rh!FRf(_>A zQ=}kHbG_Cm;P^J<^&r6F&BR3r*|9j#VarI4f-(zVBXemy;!X?Q2owro7G zNiiAhKkWA?UelCcZ|UUPNbW3uTls!dFX%%xnv1O6?tKx#$$U%lxgX3sPs#zCv7e|=;JXU5A?6wlUQM?Pt^D6^BK*=O6I?cb%>-W76l&1H9ww&dgPRiV`^)Orwh@_b7tjU%SU#=`Y*K6fU6Z1C zKyb?_;ZZja07vLj3{+=Hw$mhqmlg3|5&>Pp<~Vpf7-+y3TT0C=ac33Q>!#08`KN&0 z2-SG0_TT6CUz8%06po_!0PvILcp#x=DMQ2P>iSl4`QN|7(xLB#Kjn;P=;=8pgLVlLR3Ns}1KFQYHBA+s>MNaVvh;%P0 zQ7(8Fhf+gc^NpGxIy=hMy?{VNX`Of9CJ;@#4BI|Q-V_1a)$*;eRaEsd2~}g8WF7zM zSP}u27{L!*o!T9^o;4A$LOCzxQ>nsr7K>y)U)iztVZaWg;jz>`~4lD8?m6Qd7?S^sw-T7X2an&s76l z0#NlT)Z4vKe`Y*p8a|+OGyJZfyB8S!_y?oXv;VVDsZe#&pEftuvL5>gJxzgJH5A?I}AKTtqbDS4{9|pJP{$O#}Rp0ScJt3 zt1<+q+L*LZmbeRDm)AQS!!8a8crD&u!=_{n}-lTmQ2bZvy57W)RzF1ya#rA9TWqetb%bSXq)Y z4=`4yY7`FMdyL-p9t%aQ)JL**T%_rnocm)Va_1%Xs>6$3Zoa?Anv8Lc8cx_69f10- zJ{hVDfP z33(7kIGB@Ftw96_HEpSWSp*<)pah;{eU-0fpLUdz@e6e#PNB5D!3d~jS-SWO2Gbva+7e++a1<&S%7Mv^pAn)Ho%Dlp}S zwXkvj#tBaiu&-5>v|?yZQpjO9d8TQ##k--?w{M-&d`{G%MmFFka((zp>i|AaM-fmG z<=RE_^~N>8x`l0Q_Cg)sccsaUmQw>=9U&W?9vPvkflVBsXj^1=Mm-vdzC2sKu0nZJ zm&$dMua9Fg?3;+i6MRWWEdupT{$P6?BPkx-6DvLe1ko_RI{6<6tN&<%cEP6Cg&j2A zY-vJL@TWU8Z$H;S8{P*4MT>|NA&eVMrrq=($g7#>ZZ6y z{22DTdx~A3wR4edu1vjEsVi8hx)s0NwK88P_Gj$qwdYI$V8`$ICK~t48xf&qza~jM z!(jP=uYf}VuEan5MEg<2VDXDBJMZd+&U|_c8>hZDQ*qo>0|UDBrCYsCj)#SN&M%l; z#u#T+8YV?}$CgR_4GKDq0PBbErZX~|hP-;6&~YBZ5-uXA`Zahv+bbPzG&{=_fgg{y zeEahQA#G)BqH<(VBp9b+`m0a18O4Z?xi5#PG+n^u_`uRnsn9~)(H3oPRrT!!^Ou^Jw;+6;>|J_zeNk$d+z)imm zDh#aMu7~^wss^_AGAc+kg1lo1Q;ovvVS!))y@UtuY)0jX0T6AxC_zCm^1>zlC8er= zpy#xlF4!il7{l59Un0;)umovexn_yqt`D-6S9oLBJXT`CcFwc2u{;Qu3yN%S%$M`_ zfQ_IRN}MZQCIyq8vH%d2Z)ga>Zp&V3I+)wrc@q??Gy;ex84;h8dHD;)6YO84YZbwN zuT^f#}jJ=1f0^3M&c=BtfCcyVbWseQY6KBhb54@B}Ou-V*0ETd`P)xga|Z1<)nHmJ>|2zuI{~j}O#!phxU#WAXVt zY*?}p@VYyOjMfHb2er=24K>F_X$+Ns3@5rSU>*uRs@N^o56_)Bl; z7a*#kDF1`eq50f0ghsaDEwxmE2b(uj&M=w3D^j0P=uoeZ|y>*q;-mz-0 z+XQ7JyAh?AV0G|9Q8kO7;|(r4nNgrO#%{TBx8ZDR*A+)e5F|rHh9%M>Evl_#xcK~@ z%$>m4!S0n$Hlf94c3r}JzAYVO&oLDf-iYXn0{IU*)Dn6cnhS`b_Ks6D_7Y%@@chhF z@_qRsaJ)er)-S`t3kCV0>C2K2U9{*cysz7AV|PD04tIo%xTG*k#{g4WSrHa3;796a zBLc0s%_<)};HDbEHv-pN=+G>wn|fQ3!VA;>t}0dqD#yzk!_vC^x!-QKr_EPvgKgqv ze>lRHf#i%^eOy}S$hmimA-Q21ke*FGpSZ7>k8X?7Ebi(+VJh52$OR*{>3!iu;0GP3wt^_>lH`vBxq_8&}~-rn{!YGyqD?u}Ji zDN_7!GqB08inX{Rbed8R3x1ls>Px5Pvb&j^cuc0Z)9A_k(v3l9;O9pYz0KUUjZ}n6 zN|AZqaGuYJGQ-2B2O<|`YuYfbkZA#K#9$xaU<=V)@b_0oCDH7qWLk&3KsV(?#`q}D z=f{w`{d0sU(Yr0r0lXWYp7}FdUi0*GQ}7}W2!S>`^-;_lQLQf2l<#b^`q<4TGi%4m zapLmq%GA!SY^U-dX3SWgdpSd+teWQd}~M35y1H>wjbcO6?O0O z{iMK2Dj-2C(52?yBM4KFUp$>Q)mll(eEJm`=bU(9P-gd6fu>^zN_qECW$gYq#?i*t ziNxi%E<2$fnwr=LQ-88&L}{S0{c*0Wi1!g8ugcpwHr_<=Yy2CWck&*ka(?4s4~AZg zTJyPHGM7Oi>b^DXeR(^3hQsuJz#AN77FHkA zsEhZ1y4NP_Zwl6>-PlEtth1WOdb9Mwso8@HjK9@#U+{SR)oSfQz%Ot%b#m^@6t7~Al$SwQ^w0&mJ{D)i(M&AXd{0hc^&u0}tLOXVsYuxl zc)5P5!WWUU{K}l$d%Dg{+Ync0#xtI>c$_j?90U0tpEEPnNTbB=C^5Wfm@M>`j+0h8 zHQNJwG1sRB$*49(`V4-PDefE{!4kf$v?+7_Aa0M!cb(gIWsyvSN2t5u7Fz;p9cDmP z9nSC~2jYcu$noCrVYVAE{3(6>1XvZmz#jFcwcg?J|O#O10B}2)XNUc&mhjs0Y$n?WHb-R%1Hw5A!?MqOm&5L zqV~G4eHX7R=s_KVnD{0Alr`?D z7WQTLo0!^X=2wn}e+wG?__^|K^?C9}KD5*O;Ss|>i(;B>JscE!IrU?D*6yuBl~|45 zoBjOjnyZ!PDq`DOq3HZ0Mz%~rA}4%1d0Xu!k#bIfq#qR}hb>B^w%XIab;g-YqvB9| zAHk<4Ghne9%~4!w<~T__VT~XtDa3>7LG;I${?fH{t$o$*k1mv&!t#T=_O_=8msuFs z)Gn88G&t72VpS#<{lVT}ttAuhOFSVKI|-LydthS2*$0S0$N#-3%LG_>MIDleCw*J0p|x}XjjIDUTre;gfa*g4xg#706=Rb?Q(NZcT47JxR09qA zgc;k^nKZ25tY7Y>s{^iJWboxifgue^1BVOVVc|<6Bm!j0C82@C%{HAEC3Z)@*GT~PhJ-m^$Fn$A zj!c=N^PX@2K##9;I%=KjzONzzJrz8Vw%fLa@F5h6DBsPixfqNHN!FN3 zetjGNH)wz@xxh4dgFD^i`jxhg4{;5a*c{-|V2=9tGZjkj4xm0n?n_2NZ+*T0qj>!L z#|O+qfJ?x5*c2d!`Fi3cOt2Ct*eRoi7u5?Dqd(&6ta#-BFamJ=OM(-iX)u=rt^fIO z_d|>U+G!4R<-fAf%Kw|xfG~xH$_2VqgB z56A;QtY>chOm)H!)NYWIOA+xT2&L=etw^XBiySBU3aENx9@{z(@0&|hL<@8M*Yq(_ zof;p%(JEZaVe0r5V=kSie&BK^2Y{p1Z`s-ax6-rH=kWo`G7#%c4X14x;~dr>WvanE z15+KR-I^d%(<7rfJ1fLZ*a@Oafa<*PW(!IJycR@rCi+9tzV)GR?+jga77mV9zSt_x zZACQ}>Lx^C(s5ZjY4c@$wzD%-hm_X7uq6W(6tSU;|wV1eX;Qql$wKEoQdEOwD_ zt&+I%4-FnnfpO1*6)-E)EH0C1rw^YCEhuxc*c=LF64X8(ndv)sd&{R25LU}4Qewk^dH=Vyxu zCkq+!_Pg)B@12o}rWPoe2RoAKEj|@1-I?Hb&kKp_G&_g$Q~89S~5b>h(;K+M4FP&c4u|&Is*P zTc!n7to#SsR=aaek*36);F08=5%~!0?QdP~2Tm@RTb$>gPft_TU$)h3P>4eMSXjo5 z=m377=qz{Sns?c$HseU+jX^i{wl}V6D`S@0)`QC#{B_nD3)4})dH9={o*mmt71u|F z81??Z!89IYX9bx)!}6;-4|tUsemOkYw=89NLiSbkA09ZE~|gb1(@Z%o1x`Xr{b zN8zEx9h=^7E$@|FN-Zd!G2Q5~#a?aYq#i=$929F_=U;As7 zW3JKRb&3a?Kvut{ni!RV}X$G7%H;cnG>5kuzjCWBNJ(5kyioexta8fF?Y|3(~^8CwM zM$341?^X(E4MasG4V#zk%!KW#c?;8TL6xk|NhUy#Hn^ynq|!`HqXm1~VMAB)>@aF_ zxCb{@Vb&EV#dcuGYtCIzMm>U=q%tk%l8gafP**OR{Y@Xk^0(=aGJr0=XdJy!hn;;gQwW1t5RCS3X>8UEms1R@kHz9?#+^CXCK_ z71(EYQ*5QHNY!(Z{IzyHGc3=y!6C$CD?t>*)I`C;b3NQguMIefb>cl51uyw0!eU2c z($=LS0MH_Lh2hwe8fYtn9s<2isS3BUO{vre_8~>6>L^*S65rp+1~fcXeDHM(b>p8C z)am&vr&_@_IHcLnNGwPV9&WZd^VIJ|{%nP?u(fYkV!N6$Uz#NWYXWTJ^b&PL1(#n= z?{Fl1%^56S_ZIm+p2@Vr9;ZX^a~z z5f*I(nU`e0n;9ro*4IOYPlF}7ipyM}5#C3Nee?nJh&>Vts2BqX;9e5FP>t@!-)Rqm zKhPmUWP{Z^i>wmnip5UmkjT1Wt<#7MK1R4t)xfECxDii|x!T-#EAFHCU6?Aoz~rWjQ>x-DN0~)Bs-854x7xCy6xxU@xs{8CckxN|lcG znzTU8xr*~a*#sj{X}M1X3JgOi57 zvP&1G$ltX~+wTmRwHhisObNP66}%HmxYGZ@ttT#NSbPew!h7IH(&-qv&u3;C3z%(r zALv8bGQbG{!sgPBjN$>>cbE2BLPb>DgLfMX&783?n#AkKmvegNhsv-0d1wa;rpR?Z z?JC`66tisWQ}?cJutl$-Qshjq=m&#wp9o*!gisO#Pv}RZ@!lpNAFf2_ocj5`nrt0^jZ{nV(j_#R82rwe%cBxko0TqL9QMX_W&W2)SXxPzEV=x_Sg7RlS>5~) zYV2E0w@?-PUuQV{zkI41D~~WX|viD2%39-`vb?Q`ggbbe?#%o zh^rpVQ_BxI8&Z(n#q7=a5mw1PT$R#V3a4^NIOeZh>Y9~nKx!0N5=ibO22O=TYx33z z3OYDnu^hDdICPNhrk?e9 z>Nb%5P&jcz#5tOW3q$U=*=DiWWrw+3_uT^u)PEyTizqS#jMB5YT;(x0=Nv^-%uZKz zQ{{&wy*Q_#q9ARsvgmG3DLbIwD*kpK#*f{pzZ(l1IbnFyKbq><-R>32Se^PaX!5VM zs6?VR9@aP&6PcG>Ib!0hmPk9*ZqJ7{Od2JUFvf+FzK4FDTlA=@GQ=PA<6ar5G*3W# z+kND3un{=M%c)^o-@!3$A4T#LeL9e;LfCdpriKS@!qYu6hf()@HlDcbUbz&*b>RQr z91!=a%yMs#8VA}GX=Wul1s`F{#d~sluADkLhJtNYcqw3N#t>B``jM?dMa;~&?_K^D z!&tW_Tx9chGCZ<4HP+_??sd+`2*H-QRw=Y024nO%8%*ihbDe}AXI_=V960`iCZT9O z@XW6vnEfXMK483UdLyOkKwOkc%fbppe0KFmiscOXygV`CYq{&@BRp3 zJiR%kon;b;v_$~DD${GR94e@&(K&A7#^)M+SQfd72g&2ohIKQ>C<6D;93IcTekW?X$_=fFsFA<*nNfHGL zz7^Zm6$U#=`L9s}-yP_%cPB8$CqdEm{S-L=;by01$0oRWkO{*Y!3s%fS-&G}O^fT7 zDwC#3;uC#QUyi?GrVPfhO{YIAuGR+|p%mfGcARK*1Viaa09W4D7B^4E)h;MW>I~i$ zMWCP&nCT_tePqWfxG?c27u-5^VV*C28^xAj%jG1t(F~)qLEpMsuPe?#F zO#1EMD;5Ba>P=^;=2%|dtEQ42eS-LGzyU?Ga5;ZKuF6S6hZTsdVbEYMOHOBH8 zg>URc?qd_TtdKx{?GD+-^c|?2`(0{f)0#cJO4gJIVzu&^Q>ov#6YeLc=${BMmjf-K zybVS%1>219oOruR1Q;%Fcc4-V^@5{gog{NPAO?4(Bh)K6$|kM**k)HoDJTMb{Wuq{ z>{Q`t=jo;QTRRYDAH-R(Ind4QLr&s$8$F)JyyVdcS>fd9gr9choo!B^MNWuO)TPF0 zqE5KBlsF8!jb}_t55C|vo{zsHP`AHMsE2b~wAA&R?2}w*XP|gCU*Z2z_4e^h?|=ON zdmDyaH#LkVV$+q2rOj2t!CY@cb>wt}Q|41LxX1ULoO0%Q5jnQ9m zeVL#|^yjLx2%lKKGOg*2eTle!oD`Oj1>GV7)GKP=nQfw^1;qlk#V^{#IEs$KXvEBH z!fxmcYk;IdrE$pm5r*n-zLsa%!upF-+yeG}`OYf;#l1^;j*d7vXYft(54GUFLybsu zyz(QG5gT$@L4F-V73?aL=;3bn^A_cVTm3GvD#)W7^|9HeO*o7I{t;)%q2hE}5kp?> zB+D`OOg%A!8R+`gKIYPTyRLK0Lh7iKuZBm^#_^{3n`aOtqf4OCBvO#t{`@;4wCZwl zA`Apm!3;M5!lLsu`B2}n&7wR=8ZBf9I=Kc_NbpmMaRmAHuq0EBpw3>9F@v(hrqu|Q z#W1G)Z`uOA?i^WrB@*EH+(`NcO!LAvi$@W94I!$MD+x>Z$9v^QpRaF)X%05anuRdl zJvrt?ic-6$=AI&<`aleo&^`F6lPOTSrkNjcn2g!-V#qCMq72H4S{-FFJ>!s*Scd}m zaoGn^>P!AFG!6~xNid9k)9eaoE+iM8g0T^vMxV5TTpnN@4t3z10M28XVoS6^Rm-+){T;5zs;nk6z{;>h*36k)+cQe_nJFs zTLr*Uo2s1V?BV|)*YHeQgq>Nrxn)K1GNc9+lV=ilK@{=;@(R7)oV@@_9|G3HiHR-jVdFJRGiGlXQsYa3-kY(s+v zIgSO^+$@PUbfM zMZ29^m$qxoPe-X2->Rz@hCzGlXrJG`44!8UK8F+J74v!=FGEMrbYxn~lQb>F@vg!( z&vHH5wb~2s2d2)RxZ?w5XxT=vCd6&YRE&C)jTmDAh+UdT2N9nh&Zyrp zT;;mtHkcFqj5xHmSZ>^G`Bi;4y8aVPj%2G*_CegG(-tq2?MlOu)lNYKwz<|Y{v&(* zL(Ve^(*q8S`rGqXL6r&(T?AfZgpR#3?E9V9TjT3mJer~zkJu!m{78T=65VKDJTX^E zYr{1qzC1664ubkx3$N(+a}_TZ{FOzeC3|zi5sYUt#@bVZMh%Gx5Wl&u^|@Bj(jwcnlxO+N&$l8S@l-{NwGL;kDP3>* zPd?+z{_wgM>ab_F{3$8gA;+wO)o8d1nrB@-_-;)UgT2t&WU?{-jW_t2{Wk6z;+f|> zt+t8G4#2bZl&jvqPiqQ}aBm{~yfY{EFP9bB2D5d)o6r%5M8FLv3{d#=TqEeS2jIMtT-9GugCcuZ{Sb;%zcQ-#Fpz z652ki^hR)Jc{C8{onta_7emtm4Jj?R@NsX z_G%Xx!4?s%2V5oDu!v+(56{jD>$&&h@G5vmdzRv&MPKTjtd4ByU;5`U43w zkgRy8!?VkOdFrj3yhoa-V|0VJ4Sf>MA)_D% zX=GcMpuk~Y0?*`L2nn{khMGP!Hsm*dg890`0qHW~`87!3)fzm8*vXHx{h`jA^Xky9 zl(fW-D)e&Q_RDGYbFx4b)$sklWz?}Q3BxGeZG>)eY=fZaL)3ao?_KO@N&_M9siq`) zryVq6ge|7p<)_3iamR7je;k{Dp!$U$w|Li`UBl8m7an{OvIa_IpRwKDxY)Tj2x zJ!uizn?7LQ@R=vRJPOl~M_jEa9SyBCT+e zkUr~s2LfyhR@9rS$ax{SfCl1I((Jv*^rX=ydKp>HtSY6;Oam$Uz- zmZ07Bz`eZgvZy2VxE}ZjmS6P_=oO3i?8^{e6HtwR|C^(MsTmpEb$LcZ*mii8YS`iepLG?i=)=YnnPRK{(`sN zti6!yyEA1=K~Plqw6_N<&3?N{hC0BcLA4k^P_@SR$CUUmQom?PF$JSWR$2wcYHJPf zbyoEV$w$*DwLaB^cFb?d<$3QrpcKzUtg^~rWNH;LJr~wfE^B>QADbZ9TM?7*1uc}} zi_u}TGv)VuwYFi`EP5L)*_vL{G!Rf~%Ah9^ia%IFXa&`mw!9Xc+Ty6COByb&JuBQ;;_~o3~0&y@BW+zK+-=RIt*P^>HPe z)15ObqKB1hPUvXCUna_;5tthn<}fFEd}b-`q#84qXBJumr85OmLbR(Honn6mCjEcy z_34y5Uby)5cw08cd?LAK(H~I_<>TF%v-Jt>mF~v6R1_-ZhUr{=MpQGrovyiCDZPI< z_JN_$x;GURd;Hu-OO`FB6x1f%^+lg&%8OGwbxZjt{vu`sz6D;|6U7+w+BzlbY^{1R zIy>^dtf%)jdvHfv_A0Ia03^P!puUx2k~mlKgD_w*x_(Q9&ZQN3l-YyTaeCK6rTi;3 zc?@fwWABNaL`rS{C5ytX%3{^ zFPj-ER8gKi7zwShL?R-y-LG4yvd&RuI;YmfreF4{UOeoXY?cHUHrM^PHGwsF`JN!k z@+BehVdNC+w(gS;IXT`A0shkx{hL)t*hZu3vt1XupS7;Kj}-j{oUlM{@Jm6|4SghX zZ98jtVRv!7JXxmWxsb6o;ndeT+!R9x>8-vMS*hwjU1FdY*boVFVGh0?fF6s^nB(`FDFnN;-jb(IYtNI|2a<7uZ@Aj_}HU)`&e&$pj9MX?x0X(>;LF`R4- zn%dUN{}t5~d&K6?ldW}Ud$|>a=zK)nXkiZNPG*sHxzRXU^h~6r? za-C@DFeJW@D3;R&2|34yliHJP4r#==EHJ^`CPe4@@(~w;>X-DY-?> z`n`6=QM=jtzONMtiadrWcs763%4|xyPbPfcs+KAxzXt>&#m7cXGaXH<9aUY_)RMkK&g~I1=d^aAwwbnHa74vl$YWF$Z ztVM-VTU_}3u&-W#?en7U^1|f%1+YDqyBCjFG3}mPv+P{+|3O3;hbMKLEql3jNW3xZkc|c^#K2@mXISq ze#F};k#2|)iWC2!sUT;An{Wx8rHGLprki9B^s{IQJI`fn;`QSIz{MUAum^BES`p_7 zM`=`|SE*|TyC5g*P+BnD*`+xnemR}ffS^Z0I1kA)VuZ(S!}a=7K9W$kLJthBJB->R zRCFB4=m4qu$SL4H^`?hIbztd}a(DCK!Hzd%tHbLcKwxsuAX??9irw+&$7>^!gNBIX zUzdu)wXUkHpA0Bhk?_4|WcxG)43tg|YP}=h{Se5T;RF;YpqSD!(QLhPWsklmaAq-r z>5zyBjF?@497sMfBtRH&V9puSoEDN9*2yRf|JAVpfVO+6 z!wJ3lrk}U4$~v2=_tj3KhcI2-s&iUeX<{1^wW(_DP#*{rar^WG6WiGa!|)ZMd~Rz( zGA!EA;sdXvmVG+JB;K^QZBlx>g`MPu6a+Ud;Olt$^ovp_G8oY|Tt*sn0N7L?t#WW{ z*Ua|Wh!GRAn{Y@xXz52n>@_p6(8&l~3wRttaFdQ`*N4gr8wtbw1${FPJk!6^Er}NS zy+=i;SL7S78BqCf+;1Q!h>|*JG*m2|?aw)8b%V^!O?Djm$le{F0kZqK$MWl1eK}*n zrPYZ1GRd$ov%lB^*NB&C_qerUC*vjVIo)}!h*nPhy0uEec*`i+g4;Yc#`ylY*XKXP zy27Pijcrj;eM7QCX#8Aj1Os6@%k6MSwYY(v&_xOBAi7FKHB_Qs3$it1Cv|r^==<+hx zqGnD$k$wq!JYv{x5?YLXyoMm6d1(l%amegWQBpc*@SJYJzH;icdigWgcz$^_|Z^}plyOgGQQFKJTv=ZJ+2)kzhw{TqIS3AhW zUx8o(Z!z*VfI>8U*@gdXiK?iDn)ue_kEm^(pLruNL|ph;R4uSoJLgyh)H#4obL zB|un?z)nh)`^e@aIMnqGGehEv#-MBuIE!1zbi{Yu>5k?Gh&Fz*>zClSjr;zcnQk}I z%X56J>Xt-dui!rKFBh$@LeRuO{X7;PeG|!uLIB1J!O2YnMZuOB2jj4q%W@roHs#Rz zw`*Gjee46`!GQ~xJWmDLRgvTA-FoG&SDpwbZwH_kKGbWzF-A9;Bqrt2OLFedUb01+XUp+HTj(aoYbHpp)4hu1P;LkA zg$2D6L>VUx5^U|Fgd36OTUQ6hEgUY>!fMLp#6t^{5`SZ#8=|FaT|tpvqQH z?NT$QwzHZjPCrHB3{uS4hUX5__~BljT~q7Z8PyJNZO_+V2U&wR=`c>U zl{hQ5A2(J8zg(M;{I-pqj5?NXoJiN&Mfnf1;?YZ=%oJoNF;BAhau6kJb!S27Z;`P` zK4D1MfBxmo5U+BqFd6lWPn_mhi$%XQk(f@}_!KIeP!&fx4+F>%nM2ZqpJNSnLJ5>$ zM!j@kFJ-{Wv_;;$eK%;Wd2<`1Lco|~cxzVv`fYYQk~m=WSk`nsRHs?#%*d83TEx1^ zr63Q3A_Az$AVyRCdcuwWqwO)MLD|{kWkc?b9}skCt%_K{jWjWRI2!LP{qs7hFQGX8 z8GmolXflc??Kt&&UWEJbHP#@ii)SIQF~*7igV?o0?A>6?b{k#F*$Jr8Sqku(44gc<=pjNqO2wg{tH;k+!?@;cGLWl#re`5WeNWHAMO!yLcTxN7jupe z9HMr}!mfXi&Bkr#uD_oj5-vt#ulWR`G&z>+h7Ib=8Q+jvqy0Z>%xo+>d zmp4)xN$N%DYkrZj>31hm_QOnfw8eU4KaZvZc2T>xhgYBWjCuvTcHDN{KmZarrq-nn zopXG40gleE^}An@{SOr6iu!Y^#f$*fA_ zXB8qQrd}yyF|^D0{`3DK3|;W8c2#5uIx}R?7r?mstxUCJu0jSlZ0i7A6jY}E-@gpP z>aZ_QFpKMGL(Bb7Lu~QC&+yN){kI4Hh*RB&Uz?}(GkEMwI~=Q zuZ6*_cwCLbg|y-TnHx$!Mu?Una(-g^tRuWk8!_;^8)ON6Y*YF`6&9MO3?2#eSy1O9 z(Ckn;6y-pRL8-}u4u?y&nGf3ylR@U|4R^uWyZ%<#WNzuUuqma^L;Lu(-V49wdljgW z@Dw9WtZG10{4&7)jnmaySNBf*kFm(r!`>I`?me)H@tF=_D35YlC@2E!w<~t$4s#f5C;#A9qQ#K2HEB9uQ@R1Dq_Dooe>iaAbH#MN%G}}6 zjPmgjp562Y$_t{8xO|O*!J?0`bv)o?ErCml}&4mo@BhzUUJ(SZc#I3;D$VM zZfGsImNJ6O^+X?8Lj?eWh`Qgg6fuhoiF7XYWZNC~dP~mnHeXVDRJmGOBf~xlpWZ-t zG~`$XD~8nBS*6h*)kt-Ttl%-9+5~UcR!lFSI*UUrx0k$a89EnM+37+X5I#v_46G9VT!!NSw>8$sF+i)Kl2ZWK@y6N&jivL%1hLC)z0Q8+6_1Mrs{? z2oh(&QaivHyd_w`ohLwQHF?i(I}CROb6kFFh`M0{R zp7lFVzlZG-2xf+g0iB?&W%F3^NyVS9;GXS2LNu6_<@6S#c?-D%d{=LB+B{J~*Sk_z z2AMT}%zYc0;?Vxw^Tazg99uj+gKqrvh=B(`ylGEQMrdy2W-@n@jaUC6AJta5B*9TD3%VWZxfS|K;3$;FYNXt-F4AYv@B- z#f#CD?M#;%s+!bBCpqMwy^)MK?R)fh9m(FtSLri@^q^*BCVDtQEfFkVX}mApbYv>{ zW^eJI5u|3o0Y!9@kZoR)hzuSHN}ZMyc2^wJ*_kcQ`S+)2|1ViHkm+b$MU{aeYw>jl zD3)9aIY(}Q1qES2zotx!HX~mtMApT!vlKWa&k>yZrH}iIDSyUlZK!d4In(OxU3smq zFZ!5sSxz4MOE}R00$B%1GMz#fwL{lLMueI?CP7Z5Zt|{-j_(xn^pUMrG!XAm`ZeGX zHwJ^VFP=D~NW66=C5|8rC*zQ9%Qc1cG9NdnL;B^Y)&vCX`L0-2(Rvqtu4(TtW|2x{ zExK5%lvIACDFtDBCwD6nm89)L0DPdet+k2yFB|s_oiE_K2aN+w$P!w_*pESkniLZ%@DrgGf4@@eZm)Q83V|XOL zJ)iHsxey%nigOPV*lwkdae{{r%{L*eTxjQcF_=NexLqOYV5*44Ssyk?4019ZL|3hi z;Y(g$a7tt&&af8FnaW9 z4pI=duvRYYtTO`MnEzYoU|ovt17vrHi5V79IZY{>lhw9cu{ml4>ri}?a^~46nK42~ zU+&G2#1W(&*`|{rgKtPEZh?m9OzoHGsf?--)Z4ykUxYmb9>4bNoIop{;qrx2lzL4% z4OrP)9^nnf=rcxpI+hg(Gy4xEid>-7<8CT7UYpcY7m>mE#VDg&)G}?5UTt9VCoL`` za@d+&<&h;5Qi?LMX?6{$tAeX-ei4i)!IY<) z|GZK===0W6&@v!)3**E8+;1U>)kMeoQ@^uULl_pS-+8d)oUdPCqQXK*+`#v(|x5tf6B)f){(hk&3dUcS+XD821 z2_}o1FKv^=>QIKgc2&GQ#K`I|Z^G5|=a>gnVk#@mOf99`9F&bD3M!nKwG489F%1y6 z;PPGI`AC#Q8dOkh?n@pa`u0c5T<&4}4%ChF`@Bb=dkuG8dU6?eJ(7V)B`!fB^WU{~ z`iKV$O34yOk`^kM5g0AW5d?XT}&m(zQY$0q>rW(?V}%;Klpl?XZf3Z zn^=Eu$yf1rHNFuanJ&436Qt?L3#vXAjOo+53KIT@e?ED%*}zo4zdr^T0~Slz#$8pw}YON1?g2 ze?Adfpmhc!dFMl7js5ASK!050{_nBYfSq%ZT}n_5=dT0_V3dgHJPXa@GOdw75>WTh z+xQVPHIxJqHZ=u={kVU}&r}3`oQ6B|onn3ns})&BQVaq$BzQ&!f3V9?uZ{Rs~OWW?SeqpP@z;t&z;(7%a~9OMFlb~@?&9nnv}?*V1YBO$-onxn&Qh8KAh zPx<>lNQ`tkl4?`^ME}D!sMSd;YTs3PLs(mp;DzFg`_i-#PyDbQ4$AD7)J4>T(e#xI zLz_&4CLW->rh(3VkP`wuI zOB?22Yr(JC%=A*nO^4}St*2nd0)3bqV+t?Rf)KUWESno5$5zN}?pl;1&OaYjQfBP4 zJN`%=%2ggEFp(N`UPlG4XS{DZqHhYk6(_INtkI&;9tNy};6`wP$c=&Uf_(ih>qKkj z$_Fpy`_nP*Gnl&X_zP}iR{FgOt{DG1MV$=u&js_KS*WS?inaE;mw}`gulp9Ek!s&^YWqjzV z?%)C3UzbSn>11SkX$eJjsIixX@|}Q#k>VKIt!*IiMJXI2TIMoB7jfUdyHM|RZe5JMxe3Og-^EBKkdbW8+l_9Lok#5s6oq)nswwsXSUg*weKOX!l zmKRAebi*Y3M1jVMmlaQreBL*sA}nmd8TdEP%CE2D7O)m}NRX^k>6oOb6P58ejfwtD z>)Mq6Aba6hXO?()=-A^O7)ncHXJaH~X)3{tgl?p^Qa2%6G`qYy zj9xw78x^q?U%$B;d2!RC)wZcB@SwOaE*sFl!n=~qJyQ((Hf;ta{yMU%&qJyiG|{a% zmd@poU*XYiQYLy>@p$up47zV?fK zIBrkzYBEt^3jKrmv2G)*t4}&CR8BYa><=z)dHL+49A0SId=1GGG)73Dei&$nCS~)OJnu9$vY+f$w*9xZChA$OAFwF z9AD6B6%R~k^1m8gecPZxt&Y&SmAND*s02R)=B%;aGY_+;Lb#ol$Zm8AzTj`lA~oVk zQS9q*Yqf^k!ucwC{fAtstFca5LTp5hYyn1TQPGlw!R=hx$<#VCL3$P*xcqNPAl0HV z8TAPS^Vgm6bA#+jH4-dwYjvX$^mqPc?7&;rM!Bw=PF1fOS`K_&RK?A;X~_>zU^ZEI zJ4MWm;jPHfD1PXzM!5$NqQ8fky(bwRpQTZsIz(&Kt%mFZ3@k^xHF1Xo>8vh2-#~Yyunezia_V&E0kqtY`?AQmd&Rn{`u9R zIcqW&;Z44}8(t_pf@gLb4vjFUd>druhZ(Ik7E7G_6>ApxikMo@D!#=Z{;_$@|JQGi zs3_lAY`#1uD*~m!*iu>RMN=^e54FFK5F?|oA#yhO-;;d{BOEzhM1@)FV?)%Qib(kV z+xpNMrlR>5Wb*ByAg5SnM=c$uyc-GU&!=HY_-Zj5P>Otxqc@o*^S{Y9ONK>dMj*^K z=#;va`)`~Yq0*xJ_HIX9ec|H*BXUIgNGytI>6axh?L?|G=yJR3XuI=1-{Y-059a8&2YImBA2o7;kC?Q-n5vND z4B`q&Es9}pPFgtP@cKW9p#@ry4aaw;t0yyBpM!-DTkY~`CVCu`OLo7!FC)nNm30QZ zXdI?nF5VF*oF!YVAyhSzkOc*y)rl%fbeSndp*>eD()c1}hgrH!b` z`*$@^P>Wkj!yW(f?5u76P(wjn=%%SVQ^MPX&M7mw)r;&!rzY-SyvvjXq6 zg;{C^ZgR?yq&NG$S!WsCpjXi9;ZhqIfWeSw^9PY4B zY93^y9kSUIxI208lb_PfcG^_$#9pYl!QSqDjlOVaT#}T1L@V0o`^IpGyU>BUW1ze{ za7*J0R4XJ4vFgeyYL-iqj<_2Aq&$l*u4{XPx0ydKPSa_x-5`1CuOp2w_Wm?9Vx z)^YA?r2$2O!_nzz@7TyyBkOEtlHN6vPt@DLQyREwc$hr3^;L=0j0d5g^}42 z{Is7vFjuI!jw4XDLwmf~|9M@Q5u)~Yf+MFgOxLcLx0oX1wyu3sK0aK9keAiG0>2}P z!li3d&fT6;I>uKiuPrzKc^Dxs z!%}iuEby|c4RYHk)c_r3CCqrw&JDu#bo_20LAIn>X>~N>e+st<+lFZN*VH#FP(^Sb zoKyOok@c10+tHY|SpLp4?>~?3&)!{O6jtI04PbgbCF`=Kfu2$5&v&~N!`5yLoi`C3 zEMCsnDeQIc3?cn9C%9W7GJZi{P+j@~E#LOjfeXJG(&Dx`=JbjWPrK(Dc@%`{ob0V0 zTG1TzGb?;YO;dKVcD5zm%YB0ZO;$miPnLi(^!?gB6r>L}?9ZfWcTS!Emw`i2m2KZ5 zjN@f8uXO9{OS>xu21P0?<=x{!_p}sh&I*S2dy1~~-0g)S1nm`B<%hLT?46;wM@08+ zgYKOb`k43q#d|+_NsiqML6rvihhPL5twc24OOwTm5_UDGrF)a9V-WB|$MAGY&3#8X zvdWfqhg}psPqU-YEJ44xcHVipC$BB!JK9rNER-q^;>K&6@Jh!aIVsrc*GRnVyS11F z4WG#g0lGe$BkSy+M#CZh*hEY-6^HU@j2?_iWCt5??!Cw;B-ihh5YGU-{jp&y_T4ZD4*Y)wERJSWrS{En{PE0fgGZ4mxQ4+`0m=^ zy*m=v+%gjJptvo?Jm$St5_^$h{fiv6t^W7*v^cgPr(w9a2)lc^&akkvKB<0uFeAeE z?Ogf&VEmTuW3HFR_(-~_mK{lXjZXU|%{zIE2%nY_7zsEL0XX)0u((x0y-GVrB|>~4 zNLdzlBt3yXLMt7VpS3hnE{ z0)Lk#3`Lpd?4hY3W`yZF?5H>5Jen!*c{A3gBVGuXPqW?fQt?4t9pv~EwZmO%K%N{b zJ#f-udlHYst9xHQT<1x{Ne$XjvkK3=L_%T}@nBj2)dW7Aj-CvxWV>(9jv)ocVlwzS ze3kdVkoR0Nm_DDfxd&Rl@L;S9zLzH{*)Q+ujN)xH$dH`cljhb1OUUX&9c_dt*cc*C z7i}1=_}655y?XTvx^B8lPt^Bx3ydUF#zz7y;%vxAOr^u-M4Wja^t|(a=JR+4+b9}d zX@IAltQB!-*Y@V%5zkKO8?Ocb;YpsOS0`VG3f9V^p6CkXZz zM4QqvGB6!&=Q0N;h{BY%l)QDfVGhY<;9e(QrzTp%ks3=O+2u8>mZXFYR5_z-%Uz@9 z^qG*hIh%=B9mFLR6QOeLRKLif7rNoRLDq3_YgV)nY{U~{0rc@gd3RFZ-bxo1ktmJ6 zt3*h$%gIfU7buYfp!&aaj^rEA6lHbbt9S~I?GOATfeAj(aQ%N`k$C}FB@!r?6v`up z<`0nbQOo}g9-xw-^5;0~3qIiBRt_5%5x`UdI|c(p`At^HMx&X=?GiebxluoV#eqs- z)1r&pe1lt52$>7Lw(}x5P*-yz=3}EUV!8aY(_#IyNz$3uX3j2-ibxd)LGL02CE+n( zhJv&2|CyLhvJ!_Y!vhb#`Pi-n=KhfX2e2_e=lRDBe=1jIp{H`g>93P)#Jr6<6W5ct z%uT|wU;06VA(cDNb2)&K_RkPOHe6{tLO)hJy{LSgdGmQ(w5;9bTd)*{n}47xR>^I_BeiW#0_c8CvB@|H3~I5| z%N&4+1_flslt(~kE|X-llX77Rn2M>2`Q28k|LexXpXyS6X>!dA3%K_JQMOVDykv#T zld>WUYmRd=>WSJRBYb(O3fG6o&?kF4ZAx$@V8z)A-4wQi@zFo8RU)C1$k=U^JTEx2 zT6`IZ{`Y3nPj{nS66Z4Kz^cod7BVPuCE_FKpnHcCA&PYX*P8*IBv?lYa=?Lu*(!hl zN5@{H&TU;hJ>v)Ad58K7BgCQu9btME&6D9Rt&r(pa&GUT+y>N3Fk_>3D=bfTQjEZ@ zp1mii%3YKv6(sPiSrI?)6?8QCTqlg5f+B$e1L8+^Uk9u+cB=|Ow7^3AM*!_^STL7! zLxYRx&pM~*y1ktxR8`VmBJA*?o&^|}Z0}0oeLHX?6hue`hzE)^O8=VZjJ&sRJUUN^ zAQW*mI9O^KH~0w>$;NRSut3n~fLd5r=@LvgYMu<9QH&-#?m#@La!Su|m4zDdC_Dci zK1g}sze}kOs@Y-X+e)>84b4EUe8*av{2&!R1a9Fg$0T7NnV9Bs!5)S7 zwHBpS&NwV9Egbo%VH}bmdek82BSJs-sE!FVr*O5->2$Gk~hRBIE)@uEcjXJXC>7?JqQn|U~vvMg2oL0797g0|9bvF2+)Al|~ zzRRcXc!u4F{J#R$ZX*yn4|m$sDmF&|LSRvQ)&xm6-Dxu}(E^7?VY+y>wt1$0g>ro5 zyS$?CB{h6z;iu*qe1uC1q2>>UNODP4S|M zHkxYjGf5WmmV>T@uYz}vuuHi?pGsAT`*U#jd^=LU{HkhV!Z@>nnR!T^pG&)@1++= zAX_D)e=e&1Tq)P;TgX3QX4J|1lMW-|eOY{bhrWOi73kYS{Y-w+3`-w|F*7o|*mILH z6JdVo{#nAf3sRb2^m(ynZ)0|6$^fB~vKRwfdPX`aZjj45j@=Ik2NG&LH&fB$mYZw@ zMxpe6o?-1MRdyL0g5h~58|lFt1jX2BRF=!_701s~cyt}#*1(_to{n+`bjac3)1RVH zqlexaCF?n|xe-5ErBf;GYcIR{0%-69Uyu>Fse`{Kq+&$Ei8qGNC0iy)kerQ?j{gT4 z(_Hq*3x#xk=Cf=MFMchn+9pfUGQ#)0y&KdQUH!h{ zZc(0PQc;A}@BO*7LBYOuWK(&^-&L!Z1;^6qpA=gKJK~1N=!fnSk}wgvSVz9V#jIjX z!lk8qlaRQ|?bDJtA~HwNWP~%H*yh`GB5z&@>69JT`!oyN?vp*I8%Zd~CCi*@7n}+S z&AYMs$LwAELJYvO)}J2qAi}P+Mh3;-4@uOOIV1GOtIdrQ$t@9qh6wo~HYD(co)>gM z@7=!Y-)(gk-a_)lL4(Alr~T;o&4vgSy&7mYJ?nd`4 zJ$Q7=-wC&0+wWsyTJYrybDgsv-Cadr=g+|J4bgJlf7pW7YM;1e*p6~jYX|H@N44^< z;E-GUro*+5TC{bYb`DzKtQyyUGAqO6I^UIO@eEc{+&7hKFSv^GDosYI^DS_-Z7Ewa z1UVrD(>5I*ZuX%Y z3LTotOt+s$lp2dm^XeE1*Pl3rf++z%{iJM5N)aTn3nuB(6;IXI$#afWlx9%Q2Tzi+Usve}rrucJbT zM@;+fVtJ{Z^--VdLc)FT%Fj9U1#K2%A=-$|ka((_x6qEVr;q6z!HK+%I8qGnO8mP3 zL{{4r8-7oeA=%@dB_nNy)1n(e$PrlW*%y5-01r{Anf^k$-%u&}H!~c(lB1_SryTs* zQfPW=NC_g=%UgV^BURa$ya38NbIHE)9Z+14>g>20-Z1W$_mIPT%}zL3Zt7+7r{$b;Xj1h*Od%rXYx0|vJi>rrtjw?9;&9kab$WePh3ssY*pqSg1 z4LcHw1NrexSuU)NgbFgHyx`ie?HJrttCx)nOSJ}8$f$HfpR@D66PO}^k`SgX1D3o# z-2Qd{g8)e(I&soc6a^VXmDjXO2;xM1_Gj7c~)MPO#C3p&Wo0JJaWHJm~*A>-LZagdg<`lp4Uo{CF<@$(|p3?C!<=TUrFRfGT z<7O2o%j%Pz{iXL!({#K?ldRU4o!Pq*F)3X5ax(B8w$SN2%#QDRN+EhGPiT@zE(qeM zq7q8(z}Wa-kqiiRsaQ&#Q}yNVZ}3q>4)zScgzBAwGKpI>T&KOrlawy`54V}#^t%iMH9DM)mw(n@lw#DwC{jE6`^nf3~Izn4tSEXSia$z+@ zK7is3KNi16waNp|2-C<9Y4KU&t570ve= zgR5Yke+>0Vm7~OC>-SuKJJJR7i zFlY6lruGvz(7Q4mhn(M?JQn&?LK~;+xMFffW5Tfr9Dj*&+3%kf?OOZ@ zSLk9RBlT)*SBZX*H|Z4=%l+HFlrxNf=m8ncZFRQDECp@2s(lj$sX=9wlJ%^$ZJC*= zM^0YM6MUucBE(ag64ywF(>w!V3N7-1I=Ink?Gi^=ryt6#Bi}VABrfPWmj@xouVm_Y zSWB}O`V*;eKfKd%1oq=lNWp?!A3Ke)jGM@>Z z$q^xal9&+?ZF*^5?TfMY9lt*+q8oV4$Za}5`qTBgB^w;8*4cC>@bH)Nz*X924FxK& z-O7HH6I#88lE7Pfc0v$!fhco0vzG47nUX!B8~JqRm6nMQB+D=u&8f6(ecwP`z;H*K zHY<=EwZ-;XHS*`&VFX*Qt41B+j)dNlnx4;Q2gPdx@=jA4unbndsCuKJlFtT0@oVx} zSh39yh)KS&etGK#pJTP{NJJUWN&xL%5TZ30mOhGv&F78} z*B)!!GsYvm4~kI@GMzrQMtTtZ0=i)7&EX#;E6^mgj(v?qWoKzd*M;V3lYC{k5S(lt zr2#APprmtjX)?@_)NoQ`8plN#6w7px3Sn4}R4#LVqrED<3B2v~BO%y7IAvlK7{k~8 zMf}piPuo9zL&$BA8AFBNs%Sw$SdosMfX}WOb~sDxzVNdLvXM^WWG9@ps0ZjZ0Itgq zYfVRDply4gi)Wb_)s=6X&W#-P$Y7k&D(Z!Q6Bf_kDfYb1_`!Eb>XRx)Ef(*yA$$Gt zZ^vxoF}msNp_9VM2i=O#u!u%Z7z#gp?D)~?nNiBtFHVH5sfLJ6;;sHnSPa8>hG6r0Ke{laIZ*tTVT^!9=h% z$9PYG#l_aNrah?TtoxrzroRiZTGocSj%?aqa*EfWnjM@gUvWZU`J=Nby-Bl8xn_w| zMk_5M5{zLPYQrN59+sQ2+2J)_()OhmCRaRbDi>P$j)hAFIiX8*lTjNyBk?q4+_3$w z3qLer3nIo^F*p;Qplm$@pDhL94bR%p(&$8|WYqdK1pXc)7ikn`{~zL8!XDs`+_75m zMD^xU%{0S^L%2#bIwR9&2e&lq@FtAh3PN%;#{)1^QlASdBjKJ6K19a1CKh+M5 zvF&E#UL5N$liE4k+eDg^S%(MUC}4Q6&m_aIG)qVM;s{FZEMLe#em~MM;7uyHAtcip zmtZyLu1AJ#)07wnZpG5n_aWlrwB!N4|9nGv(6j^j3y{6Q{{xIt!@Ej6 zu4ICIhi6it;rkiEZ5`qJhA;s8H6#LWE?5SfO^^6i$O)2D$DXQ#7DG8@jvA*wPeXtW z|Hnu9A^+7*=Bog;C@sbh*avYfP#ou?9Wr0;4Z_U-Z#l+-HPWL3KG`^g?&2X4MRU{) zKu*DW1{hq>YSIg0PW7|?B4B2HbXw=k8q!A!B~ARDB?rjL;}D<#0GpHP`PevctBFLO znw{t~ze;`(Ba_bh{v^tFBtG|ZhuJpNDr_`iU>n*x>a^SoCeFe}Q5!2ZfPjE7mxC9s<$^V@+d zQGrWSP*%7`=hW}4B~=*fYm~FvK7~9xmwex#(@r5I^t#pEMV!iH*>{^FE9Hh`?0)E? z-MIfv^4+;LEAcYjH~Tb6LyC1FC$mKDn48r8ldmH~4wBRui=nlgR`u~=Iw9x%1)Ucu&5X?0e^TP8Qx-7`Sc zoY4g-h>_;>JF4au%dV)@X;HeitH#>?424%Zi~rbBVX@VYJyiM2+2rZkhsn#zhF&l> zIb1yMJIS`cRLar6*LgPCOu1@S2Kt)S(JBPyLze)n!&>c_xS>Lu^CykfD}ErMX5d}0 z()OF_1h@5D$varOU`<&G9M)j-w3&;)WbE;-K)(cwHaMgJ(7-1t;HLy5)iMvDulZ4x z3U6REaP!YyjOE*GJfD3US_S@*`}Fv*og|tG#kFu3l@2?I1~rc)$iMQ%8vn0XaH?hD z_87e|Y@$TZyRwssF?yXX(TR*bfJ0hu4^wJrV-X<7gh5O7yw#b~lMl=6l=x*hdlc)-<%p#;f6-4UqQ!p{(*-DWLR1&32JB)MUOi&8zA z+NX$DtSfz6%9oDEeR(~n{LG~r%ZUa>;f{7ycIHSD98|fE#Qt-&nL{FoGA6@z!&4RO zY8;RN1`iP}s5s|&&~pPJp|C{HnjEZ^ z2Ct^}6=^i9{4MvPOUQ<_!BP_9#VX))>}T+5LIK?f@69Rv(#`(4?tPTK-4+?v|9-;SdBGdLJyYM%^mw`uiD3|oicM!S`O#QcM37DB9t*F0=OX@-*JlYXAfh0nn z$IxrC!>zuKc#Z|tQEpOYH$cW)RId)w`*5Bw`qIRx>fbawQzukpZc`9()TcTUU*8)NrNZ+9^p*mtDdP+{OYA z1R&pm9HS1A^N%0d_A&a*uoCgKP+&ED9b?$n)6}>m*bC$&*DG5lA#mRhI`{ua)w#zr z-N*g^yUox-sVRh%JeqKRj|>4Qsype%^=I>-lPf79*_^KQpTaW!Zr)%8YP+ znXL?GhJ_#Ok|Bl(90hIZEpq#=SF)fo=$wp;+ke#f>21B-b=ngb!a$Asy)tGN`{NNk zUL%fL#j~7{A^DjGnWl(8NV!)kwcIx1t6+fA5}p`cK5lqHhHNJzFNaL|^T!2caXLoP zdcCGdnSie2CyvhU<6~u(7O%)YqOkmHN575}0-A3Qt6 zPx6;?!JpT<2MCV`ZP|5?;;Dv8nh{maw?<_Q8dq+S1vku~>&PsvKix}F9sCV*J)#t< zN^Sr0LrTge%uRaNhpGk)uJI{VZ_zv*ccuHUF+*(VPul1^6Ko2_T1$?Ad^lvrhXGOb zeR}Az?7uEsjK38oq&it&*u>He1dJHnmfe?go!7G?BGf?JkCow=0kM+I_F<)`mPO)) zgG;mwL`R2A+ATY@hV&BfhprX#9Y{78n6$N@`*W$Yhc-iVs$&%4e$%xKhBr2G!jB`= zJqHgkcCa7GeB*n!K1=j*Mx)w)Kv(c?<*&E*%gH8caXdD#&WoabWX8*Um1xV}T}K{iemq%N10k zQ(Ge_gMzHe_kW->WR0jw?~jou4$nIKldN~e_0?XpRH@bccc;lJna@I@>1shCsLS_^ zUAB7d5t;wONN-ORH9WDj_9ImC>-A{lAA^GRhs(5OfirIXc#R;%?J_S@Z7#16{#r{| z7|=KV(uSg4_~%WO0-4_Oz5bC-`g#S$!YR}fIjN73&yU{7%PF!E-E-m~qn4`LQN}ch z)~1it%~jcj;gogt9W8@U8_xm&w^c^XvSbL#xPSLXiqdltJ*PzzTh)8Hi`J?jdUEV7 zO_BYpYVaSNhymCodd>zZRK4#~i|)5EMOZ@gN1s6ehfdo$1Zc4SBG?QXDhcZd$W3kq z#a^(s?vl-N`pW3>Bn~R2DZ^M!ziO$`H@Uk@MP>}E!Xgx=4&L|_`4WAF=iRsdRswx! zbtsblH~}}JH!O^U3fu*l8$nB+=qf~3_No}uKn6s*rRJj^iXin0nUt=C9fmlwV+}xF zjTz{YuRKQEWX43*1@qSdYcGqD^xm0iIxR?O9 z*n|2b;Vs8tc=S!RsfHIoKzoahVwZt)BnKb@{+9F37k)7(j^FZNBkM#jXv7`BUHNZ9 zr8%cHgn|8&!~Wz7aTyWOh?=JAIt`MPJ-4Q+T%-WuHvLPtXG2JNZ%j|(qCA}ULOjfOmK>6l~t><#z!mtlr$KQV4AVfq|Q0bxwv@2dCfXVle)?~*f0M;YpZ z2GZ&?>P~g!=04HrV|>nypE0sNher3R_{@$u_Rkgb6h0S=Q&F*72aO^#n(WG=aeuhl z+Iw>hkCuCMMqW>7wo^|?(iW+UU9<;-)TU@fUVVd3Dc4H{KhOQx@r=5_vK<|B7O5hC z_Z$iPg)@CN%R+SD70PVg)#;xeh`1S`88@>^b8vFYkcz{ z0VA-^EVBay!%<-(ejtS%y6>I(PlS-N;@1>VY6(I*r|fv}YlQyr7_4wLn^9sBpDoUE zUN?ttma9wD1ZN(A(I;F1d76zV9qKzV2=Fym3EKVZLKfj`Mz#Fr%ZVCD2ZT*B;h*6t z7Cd|wGXV590AE6a?A9&oc;dVmH%EuWyyy)awEr;eT0sm->A}Y`h2pq?6W4nxvV-!W zFGi^f#y~&VNuBmw=4m|CDezMwE@8_}aY-Xn1}a?-3$9n4vJHJ?uURfl0z(Wx9^Yxr zhniT%nIT?Cg^5{$xPb8Kql#~^`bV3d#!DExvP`fWb>#Jt;o$cE`-y6EzFpZKD%%sDg?b0A{9Q- zl+ONle|OYx|L_nfE>!Le^P)fc(>$6jbPk4%87N z(XJU7yz1>KK!3l?0YS9myEULCy6L>CFmR=0CZ*D{Y=A#wG9S-BshV3MGylL7=$N6h zsk)MECB91Rn^lc?IYKn`y9v9p8Mm)h?%o=Tgw4f>b?@V!HM-;b12Yl#iORB6>CFHD z8{8u)5bNAB@k+06>_1!N2cid?5vx0{DZ#~5%4>)>Rw3HawvNlBJ~} z@v^_$>`&o6s)Kq>JELZ45!~+!0|GO-pP?HV$Rb`=4U&N@o+3;j#~#cEpXh938QiB3 z`2mGzGcfy*+!D_)hl@G(UO!#pw^!>pi;=_IH|-*q-X|YB*nqehR|=vtnQ)&Z`ru2I z2-I_|HcDl7fI((QCIdB5Ns*U_kyHYN{XT%#QzX7ybH{>!y1R9sAUgSm=RAj6BWS!X%?#`RrI(3QF{h6`Z`ymd~g{j`S{6$#lAzQ1& zD{oUJD~@sTf4@~%<^1kclGG5mAP+fypmX;t*qquvX-7+a+UW5WI}KIk^?NCpUR7sq zKD^Ofvw!02S)V0ZI8Rl%2i)3`0^-v@`@u`u*Y{xMj&j^7I&%8cvF-Q?t49t`jKsC7 z0{7PAodMRN$EmXlc9Bmsl~X)N3YUhDGdLj3LM?>hzc)W`^WX{ z+_1K4;Pcv$mar8rMI_G8s9Y$Sf``+9RY(~~b_%379I}(~1vy8kfr->rT-|=5=^OyG zDnd^ufm<`X3u7_fQg_upnH_x9JKt!1WJC-v)Bn(lzVeoqLlMXflLD z;%Le*?45TfYCg-IDRSv=pgVH9%@eZmr*+I7()>q*>SuD$7b(;L6*nq$@ZG?XSpaIk z?E}h&jlS^yA+q{UJFL>$R^vqUMNthigwwoO(?a4rcXaPJULX8&=V2eErOo%C_hPND z6XcJkr&0UJdtI0%5cZMQS!6&MyER0VwGhW}Obd4!qtaLz?~`gpF9YpdVkUOYX)+>SH{b;#F$}E*$;2=$rD$$89og{p zVh0AD>!+V1a#Upzzw{DN$VziUk2f(Qttz%uzon8h4)&#K=Z_WaoN%gs{+@w3P!Sph z%O}m*Ff}{S)FcF%6pSZNC{d}dbj05bE-n9M|4aseVg7%j>{}j!tbiY@&0{)hy@OzzrUqdkA1(+!ciChClk_e)pavX6T9;1iQp~Nn9I;b8WucvBuwhf zfs@M@n4?@G_Yci1?4Q5-SL|bxBs!DT={GL>>zzP*SrJ%}G{Pv@iGdOc0)eJfNV~gc zzgL%E0}q*UD&p}!@$^*PByAaovdiRjd>MOzIf)&?Vp#hF#_4L>f$Tm1pN*YK)%6c} ztshJa#J6pgtLE1cq8paazpOsEzn9@zlsD%DOaE9;Fu(bA;czG^VqAo6_Y#NOJ`gae?^b>K7#eSPqA-8}8+WtV{d-Sz zQlsl$f^cFb2Oxfqkq(J-SQe*BjH_|^PK<>@>XXk^?`l5d!WS_-3ozv(UfBaakZBiZ z5vCUy98y?mr5<0lT5_yo?n2ZTRouh6egkcyY;Ffb%aSO+n5{aY`{-dlA+rO!PPFZ~ zoy%KpzUk=K{Z5w8l%BRK#SB^KOE90Fx?eZnt!_2RqEvpuQ94GQd8DsC{#&=)&iqp- z+6LgNU(H{?GEWR&d=X$1bxt>9Vma4HkW#EQ@;VpS!Q)k8K#QrViqFqi8Gb$56w;mm`yYK%6G} z#tVhxi;>8^S=H_mF=kF1cDB6R3Ttc!UO?q(8gfb)91z zpC2`ijB?9#uik#4Ze9=2)QofszOlXvXZQenK#;F&OVqjsGWkPlfmde_bO%fCYUewp zNwk#@5}EDQ(J!-Wd~COJP|x?{MJ!ax)Eb(lVJeWXj31h+tF@}-_g2cPUw*$1(X@o` zpnvyZb)j^n_%~d@93LxAb>zb37r#w2fy3ex@W@QiCELB@1(&7CwPsDsk2;i@!VGz` zUO_)Wq8hY}1__o1)3kxu7f_*RcO|R4dym30JlEYh! z?{Sj}R^f0Jd$8hPnfotC7hi$J*RVeUo}_iLVAFD^Z{7En`L(zhUA2O;PF%>xkkLMx zY>Ck8e9zAMxLgQk{9MTaI~GHy+1Q+lZ@L#i#8-fDb+e7wNgsQQVqUFJohD!qWMrfV zY+80C+Uiq2U}H5?D#Hvi(`x%OiX-|rnrQhNt7$)K z#*xmv#u56_49ZYvJ&Fd~I{nM9><)-xLCNqwVA?m<#QPA6)(2p(7WN(V>heKdWMAOJ zI%O`B9q_?`RFOHaI!2;Ghry_fLR-o>YVn)J1DjKR0zP_elLA@Oc7;gFn=pBHsm{AB zZeA#&^|4SnZ(pX67I-Jm!I$O*Dinr(z$e2=_C(dvY++tE@`IJdJ^2LrEY+KVjDZ9Z z5DaFl*~1E#QV_5%H+I3kK-;iv_xw2=(iCIUYSy8hLGD$HauptU<+Ymr&oD4NIvsc1 z^hnx~r)#R}0uXa;C-*9=MYugJ6H@28N6&}UM>th?Ql8~&S6<6uB{=19?EGwr-1>Ky z=-iG6B{w^M^H_9fA*^~pCtbcVU|zT}7*Jjw`CKU8a3kq@vN`v|^_M$u9O>Tsi?Kq> zl(T6@>BMk&yA5nS3&&+QxrehU_@WT&b&zr)c(b;WQ`>n>)wpQay84+LiuDrT`k*&Q z$FOHwJLDgbY99|G<@ucc_}qo;{3pX=U$a%w#)Hjn$=;B^YSzRuXXjCd#sHqguJx9(u| zThzxe(3|G>DO})YcuBxpm#ON&WU?E3LXD4t<4}t^^Trr!OzCfd^5U^h>V6FcXp$g= zkxo$}i_P7Y_K7SG3UTPGceC#ij?wfH!aY(jZ8eH+oGQ`svZ$|!N(;yYXO1whH^RB* zbIn2TT9b2Ed-z7oeb~k-Ey@V3$$Pk87)M4@I#hAmWx5l19!+~={?0BzZfFnmSjmTg$L7F0h#GHSX9kCoNLIXztAfu@aoTenZOZ^uh zi^;{o5RRu^S^OTC74XUti_oexRRlDo5N!+;8jZO4pK*xMyuaTZIJV^~bq4+or%E%` zsQ-ty_|H}YkDB@YW;rpp24FJ(3snTk$Uly-I#ppNwBXP;qvOzU*kA9u;)j(U%S3uv|CR?inyN$dt)}8_Fsegy z7wliiUjK$;^54%!k#pc75YVhqBtZh6^o>Il2zp9(jV`txhg#>~X^rMjEt#gFpF!8C zjAnxS6)j%&Rq5GGDgx%CfrvG8$`Yn|72rU^f@?+}aoKNJaiH)Ut8J2l>fWd!m~;k# zq#-4Mt1(?^N~vP<>>w&Gan>>(tC(3l23Ej5_>Q2_uzMWif9f;WJkXDbiO}`qQvyKt zJuRfrM=e#5fl7VNATh0dnAz4!{we3{UC4CifPiBzPf`SGXJFl*CV=yIe|;<;ovtq4 zZcgO+L?KDu#2pOip)C|KN^_1#V_ohKWcq4Q5O$2lUK(=On&LciqlTjN6uas6s|FrELw6P z@FVlg)#ji}b~$03={U^qnFkknsuIxqpGX!tPD&lGH<-xusjI_X&P;|bIW$U-=BWw* zRElnizn|!);`J1C%$a{x@Y~GC_6_zH^*3*yeq{**qG>Y?xGVk$)Rwei1g>?c*sPJf zdSOeRzTbuOoDI|SwuWNPSepTrv%#QK5R$nOm})J$(sN>1AfnV7Mi^^D=y`jv%Ca9p`|eHe|4vHm|R84Ee?|w%E%*`d_IIWgT-C^mo}= z5-qx$9f)E6X#4zkxTyWk_>`UscmtDRE_SiNKqk)>Plu(rAnA=815-nI7QjIy0%w8} z(RnA{+q>a+(~>{hUNBoHe}_D_q;NSwp;bYLqMjF&A4X6D6Bc{yZ~;e&z)#tY3Kj#0 z%CT42L3jq`L?p&`QX|apU*ORXzv78G8HONZ2Cupu2J&N=j%{^AQAn(6X~dk_yc~@6(G+ z@%l}K`A58`*_J=6F$U(zSnijI_)o^TT|BCL6vFKZ3sP8Kw=gywm&HgoqeTQW9OCQ# zHNAFw$kP2!jyo!xQ(9{m|7nNI+%C6X-J$F3AJ7)x{^dBU_UmsKs^~ex$@o{+e8-8X zNXz6z!gDKz0Z$o}$JLW<-FoA5RTQPguX|h7vp}I!ZReRFx6EyF>+i&Oz;ASa<=)?Q zv}EHOcf3q{^mZo8q^y?JkGVM`suN>xT^)VW*1qpV5ACKCslc-fol@97;yXL%hAOEy zMG#~`8MN^Zq*I;M{!j9x54645r9-JQ!5p#AlfcYa-Bsn8;>(wK?Zb>c*3oE6e`Nl} z#-vb;cFg0}eul8FGh4ePZ4imAzNxVD`Y5JGN8Ubr`d*g;mvK#yZpA=7SBbY{N?Ij4 zvzlIpCC=M_(rIboDEny2gR8h*a58QnlFD9HWR^J6-e99iCu*Hha+k}Adk&>!hu~(p zs_h@j1ZxFz&AKV5(^-iQ*QAVzfweaP|Z897>K6ixfw8K=}>79Xj#)NkH)3pXK zwTqGSPX3EZX-c$1BkkCr5%%o9t@^fADZG`7*~odK@brn%e7D52XEVc#m9>mmhF2g1XAW^X=`n6YsNT85BwlT4o@~+E z44BzfH-jc>b`G|nk1@;}hgTY=O+dEQYrb`162i$c)U7?}q)IQmec@)Pdz zROLe2t-6$k$yE|J z0-dj=Y|O{23vBx5#K^k61v5GJ<)So@d;b#gS!osW=`ypNBJ0D%h!Nizg8}bFdE?FD z_)^NG6s-+EvT)LrB~5ji_xA@ZLf^+3V*LmVhJ5J2s&o1dlaQP_m{1pZj%mz$eT4F_PHOW9f|~c^b1+XcS=}yo;G8| zG45*~miaH;?51-DvQMJ0z+8rTJV1HW!Y=OA@d^8jb4;|G>5gsHqL=%T5D0k-`-ful z-;zZ0rvL^T2af$*N}YvEC9^uF@|BtXfKh?r1|KjrrPzSyyQJ0sgZ$Be`9I(MKUGUnwqCK!V|gR-R6BY6drtOWnmUQ8DYfum zk)QHn!_!0?=HuHhbsd(uI?A()IyLL$sg;wa$`Jjd{6JM#yvG%Ehb)8S=_R1rY1ZVy zXy`)!PGN8wgSY$wJ?gh@65Z@OY(+q&n*34NWnu}V6b~j1<}rW}3=V?n41}fQV6cRC z9bS<|JlkH=>ZQ8zCQw5}B3c)S5rVO|zDtu?gnlz{L>dJ26Bn=n#OGY|kJLGY>Sd}c zq8G{a@L0+n9hdou1vD z9iXtOOQ=6_H^}|?d1)ymkdZ=ydqe@)Ab-@>C4_i!_!1+crf#~;3n7I2utD%Nm)I_i z8wu}e(PPV7)=$*nlL^xcV^9wZ3Or29PI4gx3)A$ga_!R{U7vJ(Dl$){W!viM9-q^; z&9^iX2vyztM6!SMk zYVVb9+yFmvSG_7@m~EtmV@UKEo`vy-s{NHG9YV`g1N^4xL>n2-T`gC*;juRh%SDIN zYe;k_cvhQbJ&AO18fghl(^@a|9Z}#>n3Jj*bzUpNKjId&ZZIOWXr|WRHx}PU;!JAJ ziAmI%U#WSet34W|4WP15~f}m>BHjCeGRJ>SbtwWUf@XXVU zSn=!%AFAPx7WNwN#Fc&{%q9mCbx-eOW0~I1T)cpy^u+6J92=&aT)oKcMTISwA^vK^ zTiW?K(P-GGV8-wgI<7jG=+UcH6wlb{4jhMV4w z?j5CRl1p)g)LH6-F1ouiSqGOtBkPLJ;15+_R(aGBMR8SNsDjdOpKzdZOuMgs(Y{Bp z6mjnD{==jDxBU;SY`kFa@wk%Q(pUek(rOPN!S1kqo<0nWxaO z_Cu4>br8)sXqPWTNQcW&GHfY}58*8zL$q`6nN}1~jOJtvMeN?$*Ue{iH-Rh_92%f( zEx7vKPK=2cCoiiE z`T^UwR}rDRe1;|SGgS!<3$uFF**cl}PEPlulGm3*nXV*gi%t2x1|0OlvOTq*ImVYD zr@Laiy=Nes74^c3`MlX_PW1W!67ycuzPtzI-A*F`9i7fFzzUAlOMLcGP+WercDmi) zPuSg&!oP{g6ZX#3fYnce7OU!0LJGA6{!WDJRsrqD&$zzqUG7k68*2EfsxEjlFjUxqmF@~-6K%f3)o;DIeO2x23liOa?W>@d%-k%|zUb=XOEF*Qe$Upv z%B8xOxGd-F^F=?0y3jvtOPjm|g*y0dV-tl}LedTuA}_JENFSGBx>eOZDPm;5oA4y9 zWsn@AT5#3gzS%3kB8|J&jrb;Af}8WU&XuOqb28};PvPpB8qhmeT(asRfA$rwt&rxF z*>$G%XjYH}w{Y_zlh8HMS~z{D-%lj&a`M;+xdLTyh45Ocvs2pW>4SZI9fz~=MTvEH zFFl>EI+8Mv)b4_JI#;y%0|9l07^gZR-9y9DCaZi82ii1;o}o#!^O^KZ^r19~{$*U; zdLtCZl01_0pB@G&4|z_QSj`TM$~6?p+fMPBu3^2F9o7 zF?sZ{P9D+O(-aSyXT#jAKHU*N!s2#$>66uG%h+wbppGO7Tpjycwi3U#_p`Pz+_gYO z%~-ZcUAcA73|sb>g~cIRCw7CYHZVV`!0?FlQzAuVu}{qNYn5QvUuc-Vcc3goVeego z%#4W9>oEDwC;Di8$dEc4Z8B6i89tfZHrP{UtCj50L1Q;=lbD_@tSG9?p|HKd)<{*mTq*aqgyJa`U%gnYAC@*7C_tB4}JNZiVC{G z708IDJ2zjl+V6xHlL{Z^G)ZcrWBCUD2(y3-{s{hDvtqO2i}MWF!D&UvR)TW~$!l zG(%AXMA~`?=~Wlwky{?T@bF8_CIzh`4St+v6as@4P1xFh(-yzgA4BuX{wALJmrtXq z9R6?EBS3r;UEk=_5?#Mv+LkRW%?x@*gBuzGs^( zNhFN=TVe6=N-mfZX=uu%x|FRpUsoNFHCDv5jQsLy+o?9YQrzk@w_LV9e>W7#wzw`F zdN8u>>C8#crlkZag9Id4u1GfyNELyubDVc{Xzy?xY^z%xoOP#&=3;bl@22vmx|xuo zuSby*F%*b$6NrQYdPpq@pMMrhodU{4@uJbarq zSCYOLPL{dLr{~n?6W`TXzQmsyt^@o4X&d=QRQ8`NNblUrk(6RMk;8 z?)H8hiK0NrE#{Zg(uY}h6?r-?TVI^NxvRvtZ@FtyCN3tJtGngM{~)-_vWOUV+UUAO zf|_l`9-+m5uU2A#r=y)kObHS~L%%e%i27%7m>2H;#e*q>rzO57zf{=O&-{^VeXig4 zu7Sr&zF7<##UYDk@@N|tHd4Q~h10W&`Dx#}Rq`)~c8DtzX(G%>NI-dp0>39kX{ReC z0cDdqok|wfBMo|u&bR}T-zReTiwtrEexTQ0yubvm%cViU<`XOhVF=+E+>o|OO#PI^ zI3Z-L-hL93xMCIR38_1OqLU{Qb zV;UJ5EZcbO$SyzDt2~EkV?tA;$T1>8B@+0vAkjGoj9-Whhjj#4M3^_XQ3+yjEd^S` zy__lRtj}ak%1k6$EpLg9fH+hvKy3M&Jh`VoxL3I$#a;OJvtX%Mjxi^q1zA>rG@%|l za#qOmN9O4?%=6bWU6)&jxZRmgcV(#osX9BxMm8_N$)5sO&!>k4D=N!U!lmBRB)YMT z)j1Bo+D7mINJ*)wMwQdvht3&1QxhbG^L~kqxpIjpAj5^Q1^NYaQGx(|u()D7MRwn; z1G}}zkopTrbpn70*qxNo-~;BhJ3qtIq^{{U_06hnMJCWki*raLRGreG*PQ2X6C>ft zrb~1$_fQ8PN*`AN(~*oQ`^9N5ZBCJ|&{@U@_{OUvvPGv{O#Tx8YR8|O{MucH@0^Co z75L3I7)RL=bz8~z7oT}3UKujfp_R8REII29<&m~X`=;)ISlSQXgU0rI9M7{(JjRi~ zd0~2whZxSj1{vgv{cd+DFFDVPJ327u=uQFpPNyd;lkvJd3}~lm#+j-EFp-*B-ojdb zyGlo#Y+ZTi(P@e9T<6e7RW9+{h8aEimwz_I36QwzmZ~K>c$%{|0g&4G+iRV?-?Uir zBHsl(@xx5iRDL}JZTb#owrVf3O$w2BfysL#dUkB9wKXc<%$b$wmhV_MOx+i-y82d4 z3d~rk#SagfV95M~sEejffr)=`VJoPeg6T^UjwoP9BQ5l9Hvl-UvkjbjEMO1qLiI-x87F^wh4@-j+@`$@q zS}nmU*SgN1;!xcm=VD^3$9jJa5|fO6zK>eErzMH&F_s%#zg?n3oEV8bIiwY1p_8~x zHTZH}=t`8BYP^+$UK8p0*X;4*wjv!>DV~V(58~gBn3QN~lnIo<)=2W$;eZt5r(75XC zb~InRe@Ml7}W1OPbm?9l8&8deANYCI|g zQ=%lF-v@(lz)1;yzFAbIBkd+<5P-f;roD~Uvak|EW1c{?4d^Ct`e;7>Hx~;J^f*j> zKbye|UP6zQPwix;$i6TjK%`S@Sg<4~i4=GM+tHC^83-GJw>KrQfcXR5BRhdSNX2DF z{MJ|Ama8ODB-5}2d5$LO71Fkp1NlxY)cCPuW1uF5cSI0hH6tO>8^x;) z1qlafck1IA=)K;gK;?MV5^K17Dyy0*W2#^)=L=JoDUdK8dH9CtZsP#JQ*4tezpPCqtFu(2^-$MfX#$zFKKGKC7*u5BlDNN>%XDLBr9a{Af?#bMz<$PON zb+FT@Sa07&&Hr1s-K%a%sW$;lcUVp~=)RH&);-hxSH4t%^*prQo{S<4D;5KY`(W~v zj3K$w<{=bM8V~veoKGOV0HZck4od;!T)+Dx)zF}deEi|zZt>O+T;Tx?T4?uFSw!p> zA)+VJPlL8yZ}z-x^28#m(i}5hxokAPT>mV;v}?0f@7B!7v-K?}&%`add7^ESyT1$n zd#jkCd2z_Y)fMc{dFhZKyo$>WeKy}-5 zB@&`vcm+k<6o><=liou@1Va0O%L$f$M@;KA9nA{$C zJ23>>2fKshdd)*&V1&RlC>jScr{Q1$*q@TVM-(M5q+q<-nxn21)PnwDgGq$rvrv+u z*BX3W-7?Re!f)N5W)!F^jL}Y^$g1qSo0wN|=Q?Lqo2Lb(6+S$vuI=4feEjoq2fu3TQteQ?!7uDHq1#uOO-M1u1m88r)uYCzd$JNu zHaNQ?%BY6FV)g!h6GKZMQ5_W>w|dnambr41(Eqw)y43ndG@*^Yi(6n;r7G7U>*c)& z4$OXd?%L&yfd-sf{93nFZY}7gL??_@4{TZ%Ubnhw_o5wp9WEOlnuigll2MZ4g$C2A z)RlQNYmuRE-BQFklV!ZIsL&+3KwjpLtZFK#jftEo>osVqBgPr*UdhqhXP+yMb(~Rv zct-F-Seu*wR3Dpt`V~I=&z*xs989qJSa1j<4U!JxRmPQ9esg=*>-Nays4!vpm(ib1 z_fnKjE~`ZM8|)m&ac6dkK7@g|T@()^xG3TY8Vm@`#ed$vyR5;wJS@?~y%4lKvxf{} z`>BJ!xpSLjTBu#3_l@x?4bl=&TUMLk?QJ4|CpiEPk8V+#bXhmj2oi@M9Q!WZkl_!q2Cpq|k;)Ql&CHVi*9_qP`%B4zIl4 zEsRKlct9mufJJP}Zu%Q0A1eyOi)~)JOQumO1*8DDV-?q-0ZO<}=4imx0g4#=4ERdT zV8AgO-0V-uM?7G0%iEIZn|@4u4I)+lUSD%g>06QBEjbUu0ZAT_V?ss_JG+yc=b8^2 zmwlv8NRSy(>nbVI1^e}$??INBzgF7820G}NrU+Z?Z@&mjpFw&0rQ6wzMnFeS3EB>d z5mHcaF+@nfu2(bq;Sws(kdb2t3TEn%_v{08HP_z+U;4^jf}GcJ!SC!Y!dxJ?gW@qL zdpMvI-WMSqNJN^nhuG4oK%6AxJ;ZS+O^x?htTIoGW&qTFS_tuTE$h6t=fm9j&x#|M zJMZvkgs8J;2O7-O>n>k|D_w$bt~QC@r=yicxSm_K3;&g73x*wJ<&j@RW!t2Cp5KYi zBdy#JeKSkp$JuV>_XA$1_iYT$EWPfhfin98RSighnZ>w{&dVA-hs>@hy>XzH2o357 z^V$U5KgOk`W$6?t=3H32O&zJQ=4kYeF4PV4XWToF%0<^NUE%t)OSHCJ=BcF71r9ck zvgz~jDB?T)04lqs^6qEpFH6f*sE_AEy~s{rg$pU`hc{oU6*)mA z9D?Js)gU!AnldJRg=5?5l@+keCj6 z0&|b|>nGG%89Cw&=;93ohzD2KS#ZGn+X|m0oRK3&MC) zUX3Zn4LtO}rOT{b@K)>X!gm_?v#wfp&Yw=5p@1zG+|%rO021}AezT@qe>xh_I>oK1 z-!6UZO$3nh4lFs3#}em%d5wCSE`7>EDHbCEc-1DzQ6ZogS9x<<+oC|O@|W8D_1ES^ zSYaR=*PM~|K5&H48pC(%dvew^1f4J3i$#1?yJN%Y(XYru5GqXCdCill7W39|ort<# zx4k-p>RF?BB;y#=V4I{p1mr+ugmPo9TG{wfeBe#U`OXejk6is3-_nGu4RBYB%DU)3 z@sDl`Rj5$+%*gewJoc%DVnk#(60Gn6q|8-UIF+S>Q^~S@`2Rt~1qT!eJG4C|FOla7 zY45YRI5R`)Ea}sWU6Xo|*jNK^hudeKg25mail!B7@=B&F1xP*3TYqU~YHc}KhtGZ! z$NbvsvGG97pWed*EYA+CIJ(>6>ri+09;-a1iClevZ>Ep$fgujTqMpUD3MGAG*W|1C zy64VI<$>4OS44Usv-vjy3k{Q_Xw~emVs>c3i)r3igi%I5xA|V;x9W)L!x5tgcYZa) zUG5Xz2wd_^wr=HN;@{8lL*I`TpiFd@l|i(-^-^ zBK9*`v@13a|1_rEWQ}B)jR8|m!>V%g$Cl-p56Ylc)Z#sLDA>{cDm$n}UVIh??Q&Vw6*KZ%4#r2I*r@ zk&=ELdPmyN=#24N=P+P|ud>4qF3f8veG*6rwB|4VrD(&5>QqekoPxKZZ8gWWI!Fy> z1;^pVUnfeH=ncPk+|;h{n>!14A=Bw^yWU@&7jYR*Sb%71;7YCcI(UOe^G44{5(~i#kOrA}HW*}jr zKGFjv+`dN*)N~%ZfqLUT9_7?-G9i6hLY5q9lrT_yET+y_0-$H|$*3bjmO&E`eCrVG zcHA`EiJL^gr4tYqYN)f~Fy73fo1Y*>Pp3#gjRvKn(=;VAFW;9UT_%#S`f&(V z*GQ{MGI`v**5qIbM5(uWlERFw^NyCiefP;5LLpy`_`V05Xq>6_}aX0m8? zKamNI$;2_C9{fVJ?n{(zw&kVxw=Acx9JKL1`w_M-TNVM4H^+~t-$7N(b z16||&TG}SVkdezOi^~EgIFTW1)|^nqf(b(igr|l=XCpX8SjKUoctP>dD48}_=GkC9 z2#aGF9A;Ce(K9uBVb?&kU`8JVhm5Lb=2yjpti#dC2Dx9|=2u0fBFo8#FCh%EbR(5 z?2eT`4e0IA`UT=Hf0;)if2f^7_cI2*qy-1RCqHpzB*s{sgG6^|CGyI;v#Xhn%$FZ& zSUsK0wY?JqhhM5PHjji%1ASD0VKWDoc(hf?3d?9*4B*6&OAO3MfdOP4yttVm!?r~s zhpQ#qs&k!&13urUBL{JrdE9yL#2d#10h{WOMH%J}KB}XvRd82y%59fSBl#H3x-+(I zpnQV~Ai1RSuiy$G!3?5b)g2pIsWc~`UWub4)`J(<7>4I9QfZ$z#}R@UTIF;~mCm?) zDIEF*F|ieb*o)bD+`bq*4C3WmqEn>?-W+roL8?a*g!U(pfm9#T2YogJGfWaBloIYc7azb)u6iTy=mHyF8ruhhjn69{#^@EM&k%*pfTc&ABTg~aSfKX$0{M@rSh(Q z0}|vPc)7E=8XZN?-;v2mf=PA$&0}BpBNpQb(L^oPimUT`Q1cW^DjLdE&1Ev*R&GF+ z-;D|}x+6}V8XHUnGMW0&pAoUhVy%HfUK#zqCe-)p z2mzh>Uo8QqPiX5k`A@JNIb=^yb@o#rzEyRh=Dt;SRrI(yLO`pqW=^FZg%s@Zjgr<* z+7NhD{F)i|ccSKee4F3Pz-uXa(Of ziyR6ewAX$voMU<6;i&=s!lp=8_{)fR5s4DjwQZGIw?0QY#K7vQ;v;XKGvDv~P+4Zc z{Ubli8*|~A3I15;eR|UtodabZq{0<1GVID{c0ZOWEUkLF*T2~Wh0R~;_rV7s^&Ik` znwkl{eS2jsm9Sv5>B}NYiyT<>4-vf%45L3KtdBS|K3=e^2MHYscsU_^67({ku8rN7 zB!ZQb+#}qPZ(cY81fQjMD(}SgWg9$W^toEKjIc5`=|oj+4PG`)uJt+S(}Cm0)p2@- ze1hYuA};dP37;U|a!8Q236#{#n03A;Da^H&PXI0+NHjK9i{Gw}Gv^@nn>*uF zRn|3BwY#oYWF^s9qB9_j*tOZ=j@WO$fOrKg%ml<*7|REIAP42SGWE{JUuB##?E^Kv za&@mqhjp;u*9YRdL?LRUcP8%i(moIZ!SCKC^%+!#RHJm65|13JzCnn03UsQ+H_jKJ=bUHI!ZyGtCq06~bJD)Ck+gJqad1x&AK9KD_#?k8;>c3wtnGRoTDLjRMrqn0C&rwk@j~qdTu9iS#mnpN z;(M_DcCg2am5AlCZb?U&0Yx%Mh7)QxILt_)t%}7~K(N(2tqdv4aE3Jbg6A*Av9T>*F;oh@wfzQ zcdV=^ko@U7MIIy+ASGdl%%0qm65cA&8W4DP%&0@~xnvpNMB48C8s+bZB{$#^c|jS@ zEwh7zu$#CU3#m7IIJN)-Z*PoR4du1%{KdAvr}1rD?{U1CWei=IJ7{euH(6X}hc}zW z0m2zwC7w|i_(k<j){Fz7Ll=m*ZUO<@I362Wpuot2`38CwTh zW8Tw*p+Rl)cUJ{HIwHkv1qWSR6g?xeG0;R2k-hy+anDK6y{@Ckymj$$Vp85XYMpOn zn~TR%rx8Y&^WX?u((5F}ay2YZ=W(o8i*&$X*wl?10;lIH5g?#~WA89u5LGwVaff zlfNi4;z+eb_cPAxH1~@PZo_fXr^+yYyB3!(pFV-?&!+JqV7u z+s4DRSc?+a;OYLjLLlX`SRGnBv|%be9t+k+gYHXnRTV*0eFwr*AQ&3YHn9^|+R3(D zOeDNUedjC$jG>>eYE*L!9_45rG4K4r&2WvB;*B&Y2b2Kp2!2rb1W(p7nN@W`pt6JH z8OR@laVZS8XX9g`T?}Cj0AFC%@^lBL>QF`bzkxh(-2k!k--BkBCM?fzIWs&e4osjD zovEyuWAL^A96p+?J(vqfbcc+|FNcHd`(%)RtN-p5P{!YHR)o$^?}9fjj1NGP_z_&CtORu7;nULTun6p{g2dcY>*hG@;GNBO+*j zgTpqts|P^1ECG&>aOH(inuaj2LDuY!hz)B4wNGurm#_XT(fbtrtvhL+S#i`39ZBsY zy6Z=(Ad$!Bzml!^^PokEM1aS|_{}=KN!&0Q5bCn3tb0|_SLXL}tW`m9a)>m3P$l1+ z>aVVrI4?I3IzEC-&vE9UP2|qW2G~G+K;TV>o~LXTc`hHHq>8_!zOh+TGg?z5`@Y982;gs8-HmO zC7b8sNFmsP0zs;k(vsMF^2vjXk&>f)7o*#+v@13}t+Z(*x1_ZOpJrD}F|LcPe8OR1 zGyfnpzfU#-_@}tG%s9e8)l+7pMJ)IL=NarhGM0y`+3yB#72X|~| zvo^~l@t#k=*Jtc&tVt%i!b9#Z6ZQXKpNB;ZX8ZS!4N8mEvQJx`>EpB^MPZUMYrio%L17ViDl0saYFg>m)da+%%aI|4e2FKj11y-yF>{{+U! zXB5WrX{7EL)>oj^5~Gs(xL%#qf>eplW_)(W|D)==`#SIe9?N6$8uV0|%`gHPW>uX3|sOmY-R)0SF#C4c`Eyxn3#iYz{86F4Rd_;$d+# z0du{I?uk_(G}FcbEo$p8yp`m6nskRlm@wxkk;n8^GV2{h~VX)?pjM1i@e{Ruf?JSC9@w^p%a4913*&IZ#&Djann>^0qxNj^{gq3ORN87btCBllo zCD}DXh93%J59a&<^fnET;Po1WDWNu<%$8C5>vmkGLMet&4@5zH4E)wP3tYkVzh}q8 zcSC2bz=_#R-iE5%QHUS5kyv@_%ooPzu1b8kPXYve$UagR^g4Dx=g2ahj>9E{GA;#|uRyelTv_7+rPF02j z9naekdo(;k@>PRy;e^IbwF#jx^gv9B65_C?Q)TLYF@mXH^XdJ3ilenZ!YCtb6;vFB z2?;o2=s~ZohPDEhws6`4!+l1Mx*|I}sxaevc({pdjfCR^whBTvX*)|>&4pMG0%_sMKgsV!zBu@@!L-{YYI=+ zJvJ&%@0{~xEijriC~^`ub+iLZA1!d`xeR7;JNSF;zr1BZWkK^(1i#=y(172Lm!g{?cPmrn>VDB-fU=vq5PSvffTjkusd|Z$&8VCNEIB=eSM|7BKE#`! z^nZss*pRnD@tX>|i~3~(wxOM)Qbbya7F=)${ol_3aHHyHLqI>bz|oB8sWqL7VAE^j zqp(?X1|7hjDUl*7j&$~T08Z`sVr2hz;}#_|zv+hXCRa+2$=)i{f0JG83IWy)18P9E&w)~ zS0mGV39lKckB9dkqlP90Go`IJW-ds`{yR z$cGx=s2>tFy2lzE1;SW5{hsVjO-7P#e!fT2(CHWE7+gu#*+gAO2gW7mmEW04rdz7|6ZRft6J-cS% zr`vGbq6*{VUnLENPum{hW*~6sTIy}Fxh>}4^}Ny)M%)ffrPrVw%*gCoy{k5@YL7p1 ztT#>$XV{?UM8cS1Qe^*bog|vRoBAN&QWg0(nt;gVxjU5#v2tm#<0@Mk3G+w=Qd?Kc z1toA8r{)_V$A5Av3V+vH=ef=@0ZX^vM*OpSqaIC`%ntf}Mg;5tWx5o9FeRnQf7@Lz z^MWldE@EWK*Yuz7(~NloJ*t;A;mHr0=$$>pxJnepNTJJiF!uR|?myq8-hU?$}iKvH)m_HLKP ze9(sd>S4?3$2wf8*EpyE^KP4y0VqS(=;Yl{RQH>j%mvkKoCG90E&=B5aHyi~9|%AO zMFx_+fr`N_2m{v@SOGE5Z#1Pcg1<>aGnfN_M5qYLCT4D#f5x87u*oAV%+B2C01)F)pRO1# z9M9Og{zM1s>%2#n9fLk=j;?Y)sY+uiz^lhW(>eTpe;UR@q`kc3(D9BAEWC3r~=Il zuLF)*Xue@x3}KB^*_v=T!)fx|+r&9!0heY7wBDRZ;zu~l;A~gOq1mX9FQUShzW4~l zp9%!1QEbCB>v9NcvWD0Kw{a+?|Lwn=ywP8W{umC}I9UBnpNlMvY+_m(Tac8kV47P2 zChPx`;r+jYCAY|n5z1Sjzx0XPUAf|r&9{TDxG;MLyaJ-6B6DiH7C3~SJo+mdY)odp z_W<_qe}^^Nw4*|7l+3H``SEtD()vmy5GJSo6F});LyWE=;roTUW%^=1ImF}gdmp&< zfr#|&F}@L-qjt6O;wo2O0ww$RIb<`u5ATedyCYq3SIw5WC2>^%SBSBvU zu7Q@8NJ&x4{zZx<7?(a?yAL{J^4f7*2Z6(s2^K!+ELTG0*n-`$Eu^%qd*P@Y=Gc85 zALD~SO+^G#fFWoZsAF`L=v1m03FVIfgD62Upw8x*BZ6Dij714;?0bT&!?3ixvUe`o z3g++`?hqEd6EC+w08pXZkaZ{>YSbgyVbcibj^yE&E}OQ)Y|uF>7U}x>Ua~ z=t(4Xc=?WNKVBf@I*jRRiFBgX2bUO6Q;SUPmcTZ8TI5y7Y@TT+0CYFlzC_u80tt#7 zr3+Vd8JS6<8fA#FnBu+G=(PPYmVJHz3(1%IbihG^5K^i2`@|3pFU8xwfVu~)7sxCG zyJp>?s48~0iybYR++w(B&^d@C*py+fN$Sf0*XZ-3mw#Z(49 zLWkD#Uhj%7$sOPO(zL}ILR7AmMW*ZBXKA_oo{{kQ`q`>%6u-z*%pZ#|byC>t$BE`# z-j`A@A}2{*WU$z`Ur#6n$x_}g^Fzp3k@X;Hl(EuD9IvXpV_A*)Yl!Y^en5YVqKAZC zpB*n+)_wfZ8-W?ajED&!Ci-?IHBKn*5RhYe(T34k&S0`S2E%xA;`E@1HEnaoZU{`r zU~lZ*B8yt}HezNU&=$uSZV}|m3HP7K$Fvi$EFXPMJeL)(g9AG9Zt`@`=3d9&q86VxaL2g2be1pA97L0NVqdDA6xlew&;7om$lht<(0n1XXrpi~WGqhW z&h7w@JSYrVS526-pU$t5j-v~i|g4%=8iqAJkwn7sE)bbMQ;r`4vgydBb zv^Bo)i0xz&ZF-&}oI(O)WRPpUVnN-&@;X95P|qeJyuqpd6i!VOi)IQ-EJQZ%$mB%{ z)TrPeni=%0YOKia_;%btfk3L)ei|_Gve3A#&Op5J%)JYj{Ta9{%k3{uYO|rn#nC=cOrzIO4V~^v#V1U_se-(4gYWy=?(5w6fE&OU&Y1>+uZ?Vtl zK|+QiAfOr)E9eIgZ+W`EK+rq{hafzq_D}8nu*MN7Y?exmCikmL+NdL!!2lj}c^!4s zWouGc=ypu{DlO+2AR$a$Luc@|MJ0{Mv(Y@x<`r5a+rbl1+!AU6uR(-Te+G| zn^;IGmkWRR-GF4cu#m+LZ3_-V+IILDehwhTi}J_iy<|@U5(<=)oF@1O1VBzrN#GIx z-w}_<9r$Y!De}UyK4Vt@57awleD2qclrw%WsMU|lxQPokwfUT&KRhw*cBg4Qe0=Cz z`ThfoM~d0yH{{m?4S}|fna4SQQ?3cG!P{iieYhafe)(LhIQc7n73qp={l25vfBCjMSEeH zy!6gFO!@n{$SRs8Dj-r3x5{)^eu@34=}Y2xJ~gdr4W!{sL8^gauPTU;%Lp;@vL)@? zNFLFDB?g*M_4R{++Fp?_1?T{tJy?Cbbu=Vg_`}z6j@E;3w-0u)8R{Id3$VJrmas< zrYUg)4(6Sr=iaqukw1`iJp7DB$VLrjV>alKgYvK2o#@3bqj(cVWftY+r110lc9R#R zMQ3HYIv>GGWy!4}?&*(ecIb9TWaS_2c!B(fKR$L{jOsb;&yG5ucUNNEzBJ1v$1MSK zt%RtFy`B{zi;F8lk!Ga&`JvU2DHYQL)Iht7zw7m+d{Wlm;Lc zZc&5QgI@gd>bd{IY-~giJV)2*uIm~1STV!^^^Npwg4b0gK0Api-a_2xNE${L7O;YW zE_KR=m}kx-ld$AatYXC>Vr2o}37xP3j5wnlv8o>n_m;(33?2f%ZC0fY6}eszp_40M zyEXxa^_wB@0pAMp&KR9|f^kmxWZ?(~@9FS>uIH_W;Kv*9 zm7-QZ&84EuFwt}*@a);^=84PN!f8bH13!+|dKi&PWH+l=fyZ@Vd#L|UW!iXE-0nu1 zwwdk=;^htVI~cbi1aOSxwrzxY-ao|NF;p$Gm2y&B(+fQbrLC+}1Maa~PsDiujZhC` zH9ngkPI6_ez7Mv(4A)`tO4ntVh4uzd`<0;Od{W%6224%U;C(QeB+7QDcQ^v_YV0c*pmqh2YTTic6EKx_;DSFW-biK=kvH{x?-4p1StOBSw@LLN88eXZ&4`w}R?58}np_hZBC=50;Pj+%Gm~{1oVD}3G#a>LDv!$ zg%Wc_yh6s$=dby6txSS^oSG<5(0~;^_0UtWS5)$r8jAO#zh$e7kFtTLf(})>Q+>1e z+lU2#jwj1K5LyyIy9hay|4zeim1ycL<(aS07z4NpMa#qRZM=LbN`~}2UFY~7;zhXJ z$%;JB9ucr1#|$Ufa~`3S6iSTTm;&C1(q9MdvON%TfXDnkGU&!%6GeO*IViZ#@J3|X zC5t3`&Q^`UL>Riy|K)@2QYgyVkkyBpdkIX5T~+5>LQ0Mf#*vbt|IKvO->Y7#cmG9}a9wkT&NSM-9y?LdOpNx8} zxAhdI#G_;k-Cxg)gqA*l|Jt5_DPJ|pSmR+Ey*k_3)y zb%D!>nE6Ru-$CiaQE8rIX5l(&cGGb;!4neZ_AzJs`QEBI_l0Kn)3rm}JKM*&#Z|{; z;pN#Hwv&Pymc>{+9)%Thoe?&Zdr+G+26k2s$_CH7@^vFLUBmm*qUNB}S#5q^j-`f> zFMLMc3%_6;#vQGMl^K`a>q`GSoj~(~x~;=n>0+QkKCafUk-PD&B3#I#tJ!bcqi#}u z^SX8OgwYiu#$&EW8GpyU1Z;G8r+yZ(V=lxGL*7ridNQj4?woap>7QYT(7`<%LXsE* z2>0ByDlLoJn)rZ-p%{=KDCcFz9c@epEP&f+9s@0!YlIwJG=dxSYKx!MKdd?RXqtF! z;k!24tlxXY(a`)dQh^>b!?Ragp(O=fiHQe_u)=h|!I#L<+Nh9d7LJ_zxx8VW32$ZL zs=QMP*e67?z6`F6jcj1A#lcWG>|)jB5ri4ri?DonYB4_h9!|PvX0BeJIL>MNt_4RQN73G@yt50-^b0|1AJQ;wO*{TqK}4Vb zt2_E-eLr566$Dwk;&3o9?n$0BSutWL{i#TL#yem0Xq&z{a!(v= zZQ3uF{(R@^iAbzb`x4*AZkxtYnwDWh?YlB*rIuqvB=)q!Dm0fSp)F_(zd`m4vi)X) zNZ8H$>H5W8zN-#Ln#BEGVBHX?ZM4mp_Upr{CETPDKK-wkrtgG$hWD51*1icyB^jCY ze9o-t-t(qaIR9UyE0f)D50*|J667#oxu%4EGl-c&V4y`ef*?=3BMqRXt~^p*Gg|?? z0u~9O#))-c5|}EqMvU20G%9B11AdNYGvZ`o@pP*^mO@SqrcImuq{dg1^reGGKy>9Q zuPq8Y(VVa%81-@9GIb}|8Tt=B0ETrtIFvu;|K0|`o~tUzD}_HG&eUGBNP{6^OBC}- zH^3LaoQ#K5;vw*2#SsZBE`fL_iw?*PGAL1}fORSeuCv=`iR=z1wJ|P{ly}BvP(oIg z)FH&!4j3tM3B%ozw=@~DS;^X3$H0dqwNNoDFh_Pr(kM^0Pdu96_fzjl3=jZr%BPLO zd1U@LQJ_N+Jc}*W;%d%fPO<|LR|QRW?*3_Rg= zJ+^Z$`*sp-U*P#W#1F`a+7AyWGe?%fv}N>BO>CKDhtUuVj#jXpWcT*Yw{7-7Y)Pcu zR9Z+&^~I}m>XAS4Gk5#&KdVD1vUx?l631SNen|cngy0{)8B zYvZ6lBt&mYhk=f;Q2-i)5XV&c6PiI=u=TDwM!GTQG!f%f#W$&Wh_>;M0WNoT_C(P` zu{(nerSUM_&h>JR}Bo10uoEp{Ohf60f9oG9dUW{zw@> zk|2Y@d;AXO9pRmGpYO%|=3V{Zb6*iu?ua&bd*bnZ=r10=w7Q5nNI;6Vm{n)XIBT>; za~0K#n``{8L*0!_VqU0t94U0uU=|84Hdfyljdp?DrU(i&WAoa)PHmSWMTze#Q>#Dl zMpTGzs(g&RP@PH3%CJpNhXvdt+Vq*mH(VKE>%==d;3ofdP-W@}g=v&FW3O4&9{I}4 zLKtpQQ6D874m>lUT0qVF|A~ejfkQMS!^=WecE$jY_ur!Fzdte7Z#wykd0%>_?!*6} zHTYBdrAG7TFpcmnoEDvUP`4#mZ-4_qkYHL$CJm%;s3R2&xc~E`4TW`8K(mQ+(5`N= zR0K0ITBgyy<@+O})IzNd6MI=cQS6ER-QYCyZ3spud-<_A8VHQd-o;@iIa&xAit< zmh}Sao`pe2)#>qeMMC5Nh8w26y{g&yPf*Uio14W_$2%I(OlDJ-t_a5TM z!ADgPR_LoKl>>0&=pbH~URt;Kx^%ULvfw56KGicCN$lgfLPX6a4f~c%+o*_dC#n~h z*MH_|J9e-W{v?hlOSF<`;;$U1o`Qm`B=t!=&;gd8P_Np*g7POzS4OzvTKP-5^fy{7 zD?Cm#FZRV{!Be*nHLGKqSvRL6Q9txbj@*1L{1f=0f2u{4l1;(%vC*aH3|TN<6E04^ zWRrFrW1R)wpWpEzn#pw}O?InoxyxyVQ=u}`CEA6IP5N?w#3p)FPYUqR6E- z0}3N2t$qdLItRDy8G+sEv7m_gJWgbPtI8+>VgMz_+YqoFL8>IwQg1LW^WiVIYN|>Z zVqpML5FQ6)F1GXcKp0Gl)Y8BeY?jvuamQoG{fpYzie3`bqzPD2$v$|;*IY? z(+t+`%0hpEXEii33{&%%c37(sA$bMt6{X{yHYWq_}l(bv=#`5*odw-JjZ zkJoi$BlBImD~i{zaSn05>Su5WyRY;U0g0*KQ)_tRqzWs$Wwt4LGxq=}=jS5JF8+&f zQaVlOkK@-`y+6LyecF;ao8DIr)65%HM9>7NxR+yA)qYuWIYtE@Gzlq=hKhyn>~gN0 zm5ctmI_^#88?T~cgWbpa|i}#C^zh+uW^b4Lhu4Z30 zrR*YHqWhTNzY@1L$^v{U8-%VVVeCC7djp5XNpRp(0@8&$qKw9DPXMc`*7N3%8p0Je zNe?dL5UZOlRg5HKqoGK-jZqfO%=Jw%`$V+(Vw7Q^czvS7dRDHGh3p>k3`&ZMGi&jv zzHT#EoyMLs27P@Yb$OaKYxjdgHWO70%L6(dak@d70A4mM+jW_8!=Ivq+yua~`L9V0 z|F!1Wk%qwJ6xYvtW9nmqNZ1vC8P`Kh4@%-9lOMp4ghFr;^#nd`P-JstG>h*X641M{ z4)W8qolPrYZ@}x9z%CZ=Oytc#Q5_zyh)KIvQMxuHjLW`e!NEY+qFT7%0(rcJ1@s}z z6lc1$0v;3;MDX`U8DL-9=cjR06Gw*mXC{TZ`|s|ANG5m{xTG+Y6rzXP1;YzLSz=R* zA9&h&4nA$ay`^wTAK&rQohcbq#CSarcc2%9-cVbp=;#|;dZ15QIX{}+D4VwM&NCu2*#J(ar7gCB%=%Yf4t{Ifx*1PAcX_TvQ_BPye$HV{(WCnV9Slb@(Ag@T_UU4HJfjN&g zN0ye{(Z9T}NPrunT|LBE!%xqpgl`Y2hLG3k9l5OWa_zE93$N|O`2^jL}JcV zrtmL_=@v;e?9J)2<&b|pmr2qO^ zxxw@~5YCvE3gS`L&q?ebiSMbIn8BQ^a!{}T*)5I95FFP|2|FhudX3WGGJnfk0AV*t zW&A^X=V+6r^+0fVe95j=fu9~!johaqnUjjA%=5%X8UAVb{7}}`o){cNbwVu1bl(lY zixfC?jIGY%;roFAZGUIHjr2si^Z65T63F;E8FbJe{8Nn~TEvwZ4@Y9l@Cbco$(^cx z2}dgqREWQ!OwA+s`8|+QdE`6wvEcH^BI(nuBGW^2UGWUqs}+m2fE5L^{?uLwE@+fq zq5J2kd8*RzGGtu9q73EuN&W}ylz_9t&LNqu`WRQiWPIP&!3GS)ETJG9eI1#mE{u?z z`2X1)O3z;6$85;qN>!ky&1m_}S9daH2>-|5TPjZzWgHhQx}VYqZza+H;;QJKsOD(x zdeQJtSGj7Hq2 zkamWKNA#&gU}qt zWpC%bph60PmE=f9O)JQhu^Y^W!V@tqHExtypZY}(x~}xA5hEb^h#jwtbrNR~F=n~> zt)slopU=p1HJI|9f}p(Ursw&H$%-c_E+4!4-T&ceS^dO4^D#=(_+CbM9wFHJmcK#K zf)&~HxVH?Jr34-8je^LiU>a80GG3W*Ux9?Y>&$(e)Lp(LNnNxS38JSd^S5{9{lFty z)$3MYpDvbIc`0LR^mY3c2e*Y5(e$MB09I;Ddz?;pzP{G#BJPX9e&xc?YTcIg7;!9h zr*{=@l+{u8mU(u+kY?}c&z|#puax^ynw$kYJ1@oKBu3zC@+F`k-;qYTd?4;C0_p;? ztu(1y!b&qSHR}@>!A5G(<%I(O&oJBRow9tVq$T<0wj^HK0}~yaw-NV+$l>^#Z3~VJ zb1;z*!YF}GT83YY-}gp%8;{Fg zvZKGuDzjLSv?P0f0~UyN6V|Ve3`K1S9o5bd{<7a87A7K2@J6L}RXWAiF)~`~QI5@A z4~tuu!9!xlU7ne?w@m+<{>G?dbt@b4&mdnnmolvjdKC1KIZ+6-?pRWba=&SnHjpO7 zHO}V`lajjYW|9FN+>Qxd9coo?buWm@V*Y_e;j&6gLNr-J3#peMBWpX zg(CAPBlLWS3y*TCG&D}w#ksV@f)KlOkh#xS+LaSjZE|e2)nr&fOWpk4`%tVf|2s1b zCAz}y0YrIX5?1f>KUk$DKG^B0iOVyveX0=Crm!#czW?vuhw>!(n49wHcbWkt(PV_` zeR)vx?KVYgp!T=lS7*1|ptdD|DY|A1yss(!+@Sw6V*;f`jRI!~W}R=ACU8!mTMcxB zg(q?Vx8QyGi)Y(kQ8G$Et?Pdyr?3I((!tRMjWQ4$u3bMj%Hp0Y|8bIdx79lEBkkxZ zE&>MZe6pTOOA4?pF?%OJkeG7~4t_g(Kh5JMsjVI^{b0G^%ZKPi1Xvoq<4V!IpAE|_ zqB<-TA&E`l9SM&lFSdo2%9V$uYP7 zFgh<_;3dNw2%(CBw%sx1loBsO+=Ncn%{9Ro>95}F+qF)FiNUWQ8&{0>1pov>aaK9v z7lFX@G3q=Ijb0%0ycsI7^72xWCTnkiYZNU@9~uP`gyP6HD~R<-?;&Ct}%ebOV3ZE?6 zj4kPsM2hP~4$BAL1_+~|9FUo~I(jFP2(~IDet{Gu><(tZVPg8HeInBSxF=~QX%SKB zT{8pHTGKBa+swheRRMOpK=|uF)!4uYVh6BKE8vh!_Ouuk_q*vF%JGQL{O-eNC6QQPq#2#=^_aW(N~&h}_n{4~GgGWfs9E$+%BdFu)r{Uu z3H(3R=-YUw&Fqozn74?3p%3|ukur6y-XEyy1TL=SRFPoM*RdRu7t2=vn$AyG??o+t zkSZQw3fI;Og{VjfaR9m^O75I`nQ|(RgVUWjBurmXs2ZT|10@B21Ws$iz{J51RK zt5#Uleg6bl===%CIp7GtVGkL5m;c zBnaxl*l-U#xKhUwn&w5L$%z1MDO7XuqJH>y(E0+q2_qk+;axuxGCa;CU=G=R={fi@ zQ5ba#wkv}oas=iEEFeg2w*J*1HE|Re`jf$C9w7%Og*cKh+u|ks>5kyhHn7cOj?rLJ zgnw%F!z-ye>BzCSGTs^tyaWxZ%8BWh9sU}GXLa$`zkp04;T3O(C1uYvvj(2u8s|Zk>4Y8@sECbK97J#>Iy#~rGyTseigWf3 z)~uA8RpQge>r1pVXD6Y0ro>l*y*E;+`f2O80b|rSbVXz|V{$cZ<_oENVG7$xP<5&U z{IL3uJTm|G&y@{kvKbInY8I-%&`fN#3dE~!yyjrF_-*s%>kwZXXqr)L zwUeg=N}sY-AqxW~4oTsba zOnyqmWii&uru}GDe{WXxVDPn#o-Ja)JBR>Vv@9IZyZ@!6cM8 zwnQDIL;V-=1Ys^Zc-z-G!9Kque7cnV8Ii0K7;vW04tx5~AT2VFJz$6PT?GTXF0StjfWNTx2<$Kw;%9 zdI^(BG)XDd4xm0(bH%Y_yCUjpIP!o1h|&bWi`@R*eiQw5&U|B$kQD0Bt_9d{XSE7h zC?UAQxkjwt_f3dO2Byc96W9#qAG?(+4(Vr2?5^e@tSr`_&>T%jWB2a(AdBbpwUeGAx(P5>RQko1v>NfAJA zM_UM>fvJ$W3sAx&*yaFt44q4-Q(V|V%}IQX-zXL1_Kuvy#vc84asum)h=5HfEa%ab zRt&le`(-=dxRA|yLU9(KmPg#v$$zw@ftV%Eh@iU z=!qr+ErqJnMoHW!`X1~VU!H4vyEm;v1X-Sd>UrC^zFl+IaY2)7r zH#@`?+`pB@Dib3mTWr_!L&(5%(*Nz~1LD~&Uj7387>!s4TRX^u5qjr#z$T$^wMbAv zep1A(DDkb`(#O%N5XV+QXh-A|rFew~GxN&;EFKrVt$_pC0bwdEg_qu~86hIA$*s>7 zxr>t1s_fqK;0q<05??{35fqlu1^u!M>-9NWM)TLS#aL3B8wZA$;t}75#h1?3==E2u zkhg1HI~uNi6pm8Zm40h(rkWkwsiIdH{H}wY|D0rcP%SqhW>5lY)6mT!)FCXW0nlm& zM0X5OI&~Dt;$x|rii(H^o9~oP6x#%)*HrBxpV|rOgpn5+S}P>4gq<({15E$>6LOXJ zlH{ANM8FsIKfRl=Sg;GMmz20HW}}qmPHUY#PSh{2KMqGaQWXb`3_0-SG7(rf!L!IZ zLXeQKkh$MOix@EO!e;=u^;}qQg^r3#qe4eDlK*}5dZ;oeL7zBP@K%4>z~;P|2`K9~ zhACVjaH<3K1YNdtGW(13)mX`-;4J_8I}L<(x2hs%4AFiClRUmD5H)R}FvYYZf_h_k zJOVtaPH&~@QULK%Zt=t#{w4`CWkjKiCb>yVFy%z#5BcC zZ01Ma8V(lBgZhuPZXCB*pg?Q}in=8h(v7dx1IJ|`o4iLR77IPs^n=F`H_(QIU6{mO zJ!m@2D{gZ?xjWNv+PimxLekXv60^B=B}xaT8} z030sIw8}cuRu&Q*B?l69dCIaQ$7@smeh5Piof9viYfpDr`6{!DW4o{qL}+n+=llhC zQwI+l_EPmr)_GISW5z;k%x7$IEF&81mT(KgwQk@&Ee6XHMi{s>f6@wfX*d_~r=pkS zv`|7z}NCu4Sc`G(TS(Pe{Eb2aH>kSm|FqA%B&Cfm~f4_F18z>NIix4{1uo zfK9f~48`TKzztLJcJgdkoL9X3GUD^GU;g{Q$kLtle}o6_Y{vUy_EOK95uc4PR&V^_l4^$+vd&m0gl64X8RTePRwqS z>Fn0CtE3pFZ(S%(h|zZF#LkUrs>yR88s&XB~afIKF>Ux6@%zGMNYE-{A-F)G~P>)T*^U-XP6$x8O)`3B$In=L9 zEt77%nr*}&Ts%x?^cOJKOMmNlV^2*J8WMF+gSS~@wd2Xj*mDA{U<)yF(i+O6y9MEp z%yRE^!#l8RxMGi%V`<`^GIPIEB&_}8Y)u9G_M$^(I(K^#2%)A*H}(rL-&MzCcJV`n z(X%mz-kz2*b(w?d@RHj+yba7&$6Vs`T#QspFCO$?N&_v=WNG5trIKKzZE zWb}qO^Lp<@{CNAlUpxMScn7iibAOsV4Yv4B5t3J#b6ZiX4vton+{3gm&o!kxGmogO zG;r@ONmOFRTc~aWb#wNm1L;w&(<{n&i4nMl6);nxo#fmQb&C10-{h2d6LZiw0 zJ{>e5YruvbuqP?-Cef~cwpRrRvJR{Tx)j*jP@D!=@xFU416;*n0=VXmu%P{A(Lpwe*Ec}+@`#F7y z&-QeH-K0d+&PGUfk^=4<7{~_bG&5?u(EAxCk%S2L=_+sZmBrW2zMoF}$9|ZW;~K^e zB4kN3a5LUf>i$?`9n)1CgwqGb9W~UeJh?y`ad^;c%?W%*w_{MgttYh!zH>7r zxT_kpxkZ%mYYMIK2qh^(TdFLBeJ&I>zz^#I@^r>d*@o;+t=Koj(EanH1|{0UYz2aW zZ=eS{9Ks;F=GfL_;YDx6G{_SGwk|5E_%?xl!$(1quOG0poZ^?Gxp;pSZ6?oH9KU=* zH7BK8L~I;k7E|$6ahci3MAd3^ZDAz=GSU+z%sx}gO;SZB-c})}BHujf=YnAhlGIz0 z{84R!dY*|!u4RiiUVS7ML;?vaUXmen-v7J#!t-^&f(>b#b3_3qH16BextB$_iQ2&S zsGo=ag6fe0{;{YJDsXNdPoIx=E#HY>pw5_!&}qGTHTziyo;Cho)-V(Br+NvOfg-od z`;2Mg)!Z&J=huSWJm#IqTmR6y z8S#hq*ZDTstN`uPMa!B{tQ^-&1vn~@+8j#luK?pJ+Vq}CR<`xAI@V)nTGlV-$k}7* z(-3mwO$88+b)?vVVn6P zQwdNLU8OMq{{li5U-VXiEx1WU{#ezh`E1az!f2oO`!-e@_#HX<;h1HFO)Q852#`%w zDyTRqUj;xIv!%aud7c6Qu-{nH6O@8`l>4mQqzmqC$PX5+d91W*=h7D@tAWPf{i*)u zzlgZ(+3L*j?9WP()3~fa8gD>$?aiOLPVCO@1b3#rX4cVyMG}MQh_C<4HmB$a3G!MT z0r8B^2=<8kfj=FOI9#2b+rYgO{=|SR+WYVy6aP#0qgDdv)pL?VJ=Wv`buB0}!pV=n zOdL%kUII`;M_qyy`*b3eMB^|u@Zd@ZsF#i@?91jN>z-KL)*C$8WB5c|lk)X<%eTww z{>kQJJ|zoiDzaa@p`S)nT2VsXllfE%^cP+(U7-64q|^;~#-OOGJG*R*rguSNFy9u8eGe$)W&C~AC>Bjj4lPXRT93Ne#o z^Z9#`rTM$$1c~gPAUj|ezYiD^zwL`LAb)vjFQ&OC*yUH`3jP^kEQ2-ps%)2=@)@E_ z<24W*zg+(Z_H&E)ld*C6Gg4?KpHGnzB$=&tC)Z`iWy9ZDBQGhQlgrmN z|MX}q{;&(b;S4!5X0>>$fg5ENVZf{EWNJjZ8r$f#UX4&TT-vNWa5?EdaDQwDkM6!+ z$9lU#IpU8faWtDP%0gA+2=DbqS9Kog1geGfh<7lMC`zpiLi|qsRG@Oan+$FmN6dhW zC`igrqOBldf}e35S|i5Q>7V$-XJs6Go{O04a$|b1CMCjV1B8G0b!a$_Z6W7z)Y2B2 z-sg_cQcB~TCWpTho&lSsWy|}@yOxy8y42&z5Zj$IJRH^(nSK(nSJ?6kz}wI?F6Ub9 z=WAuBA}wEHUh5n#;GJgGjy!GlcJR6-n?`Mj@wWG_+|JQ@rkQOh#$92Z_MIiubKaNy z04B9S;}IJ9>o%}ktEiZ%o~(q!|X>LW;VY1`I=~!>>~SZp^v!qjLO~ud=(ZJZe&+AEUz`%`6|Zh(6*f+E=2I^ z1_YlS1WN0ui~A^!v1^DJn<_q0oPnE}83-kU1j~86nO*U2JYsgU(iF};Fcp!2TR>WL zF6?ai?HZ=09h6xL&ju2Nd6o?xWssTR0-%lqG}(v1;;Rcq`aCQ0V}l0V6)N)XpW1hp zPosINJfdCf%nXuI)O(fQoxH%sW#;8Q3;Mm3c*P`*__yGANI-+_ULvqigUBw1#E-w| zx8oW4lYf+Vtv3(h`0~%vv?J?u3IGC#N)24EKIymQXhlvv1~;FeIr}&H^u%6&9}aGR z^zN7ACJWPQC!EJP82xn{Lw>aMu8#G}GXb_#KThBLfGDC1!iZ?T=U*6{Nbp%;*;OOIzT~}MGON61mJA%?tDlWT zJPBVdMUp#fZN6$QuY#*Qq58b%s?fXXQl!LYoANMmTlI%^btKr`S`Mc3HaRIsG%`q% zeRJlp1BkHD#g*|}&_zHXP5ZkX1>P@I*}Mq>*nEE>FH_)V6@%KRUIM_nC+G-}qt$Rh0aqBRxLgGtf^o)QkreXjoPNbej@DXCW$ZU1@t)1p z4T~D)0+IOdCpl(Gy>cMDx^Lg6086k3l~|BYn|pSiAuB?KNO<3%{4vFNLR2*X07c6Yo0y$WGMNdXTPrt~qzsO^D8$B8X&vQeYs(S&c`z51O3Wj-kPInj~JFMM% zPF)urVtWhE&q1K6Oi+RoJLKD#8Z!fJCggG)n2|K8NQ031BQ~@YpnU$(Kv_DpWx9{W zYj&k}-s(RxV;G{33dM#EHdN3U07{H&CUSd42K5gu=uQ3!XK_v9d8?)W z)aKe{p%WqwZe&s@jn{3W-dX!fP_pJNFnR(ny|IYO^p=a#H4kLknIOXe!zgZfW6ku% zJKrPr`Ys=%u3V!me7=vug(mqMG)H=bE`Dx+kuG#Kt-_%CkJAw<7o`@vN~8z*FHV@u z+;xhIu>sdmvyn)Dz~MsAm28CIRcPNMo_&40Ec<;;Y~b?@_OkppjUMR%?}dg6#NAZ( zmIvArjFo?a!wYc{Ts?NgoLsp^As|2@w~G+cTvZEaP+<67s5YqooJ4;NEE3pP*=0do z{E4y-R2$$bU7wXT6p54O2HzuL=cJ6Cv1*7NtX*K7zU_o)H(p=+JS*3=Tb}3|p8Zyg znT^uxyYt(1{RJY6TuGtJ;#)vf-7uBLatJM*V$!LV+%i{}HmU56A?D;U!C3=Cr2h4k=iIAvM zC$4o%k*N38oV5?S_Wi{U zoZY+Vuu@Bg_2JdD3RnKsW(p_>O>dY^4s3Hwp=tM~gq4ZIrNHzNoQzx*iTcdwjg%+o z5RcPsB>fko|YW_hq?nhOrqv1NI zlEjJqk_ANYQ_M`Uv0|Y$8CDQrqkvn=!V{vN_R=Nk5|2LZu353mv3@= zN2?|25r4CM+E~2EhLFhiM!0S4VJ1+O?B~3fHoq6C z3HqpheN#rbmwm;BIxvCpn1Aoa5r@?5f^gks4(i{^ws=smK_B~%o)gXcOQhfkH`G51 zgyd!i-rCw_{BpcaFvMDdxHp`)e?!ZR)EfQNK|tMPgrb?&cJiFdOS~0LdGi|ivFtln zLBHqTpC3T2)1xXa3_!_5S@`K;8}oVJW+l;|gqa+`mhCCv#?kx*$+ulX5axxVv=Q!E zuOV@W5GnJYb5ZkCP4EYqUN9j>V_UP0qt!B4eEfb=*rj&2XvzY&GV7vno%2sTh#BO; zz-+!rH8z@jye1e84mnb-kAf*T<&ViogGDah;Oilr#aMXI??sI2pNe=Dr!X~MGTknu z%4je36>M1Z&D0sAbuoK(RMfWp`BL~SrW!73{#gbD$+IE^8V*8g1&=i7>+8AbUIjM$ zI5=#us3S~iTwa` z)1niudAK_gAk%>2?Gq)r1#4*1&8kdx1cimrU?T#|A+nwvX~iDLrK}mR@qx7{0_CnA z^zUKey1j=4b6=A~Y%IUK*x+GE2Yn*3zQ-Nbw2rTSZh1>10KHSVlb)^#-zh}EcRjQE zd}f3`h|0$Ko_W!cF1T(;6=_?%#|6qY`ZYEC_l zTJ4a+NKc^|K4Gowh25MW$Q()ADHyh?zQ!MjMt1&-2-zb;kzj14wY{{X*keB45f93v z%1zAx{<}DPGzyDLuMZND|5_rVv@LdS9nvSGtf)^m+iR06conhwDD@i$|4W{ z;G_vxM#z@FE>%o*t=PN&H0~YPsb6K*^IytVFYIX88vwjE-E+sHU4#*Ei`(Bxm&Tk= z-}M)8l^jd9l2O6$-rwo{WED+W)dqdkt)855JaME_Et+$Qdy#sPqz8^I2IHG>MT}*{ zY#H43zD-`((!Fdc|3FrGAo%T0BD2-?rKN38(0CjI@~F>Y;CQv5${^9gtkv#AB%Z zlTR>K0z4?EB=y$e3Z#`6Cl0HYGhOO23lnPJg^7~5dXKvEXHTd9i=?0UB2$~)0<&XC zK#poz65#iR>^Bx7YHq_;f02FE9CUTq4k#)4Vw225jc&qWPdrN;h$?#(C7X~RG>l@XiTQ)u2VjCrAnf84)_ictMCyaY>d8WqKcJSeBuulGb?Ay!KNjzZTOX)2%!}Z?c!BTBeVIGab^!sehQeOfAP*3cW86$*`Drg#6ApK<~*K z5z93U>^t<-xtc8ssGdh_+_S=N$+V^yi6h4QkP2`*o9^*Xkl?+=3!}nagxGa2UQDzl z{IN4)o>$7;1l5Fp%^6DoBBFhwL{Ehz3u%%}doQxC8Cy~ZmvA;WtXpKd=qq`=^eTL! z4Rf+#eschq*ufO^F$!Y4?!Yu=&$MEMLkH-V1Cd7+l1cG!UBwqSF2% z?W1BcvOk|LG~It!xYSGguz}CVPHd!*ljzC_wcur{Z06K|pu(MscGtsg3({9e5UW{7 zx+UdgkO*1Tr}RJ=vj}lXt1?I?yD|&wF}eYlSB>y7y%iB_g%T*HsS2uvkN+kiNflS;*vYrx`Vg+$(;Ka(`lhM4sqFVSAC3P{3<*Ss(&myLz% z{V8iB`*pfSOgB^6SQrv7#pfyJx)7}xZF6nbJ{>=P1dL`$IBk)@y{sJpov9JaozdEW z*ve7@A;uWX{9|#tCh?dgsxY5d`76TFdXh#Y1hUfMD27lK7^@9{Ya#&4a|YhonuWxC z?2I6@9pUh-Ixbgfi{@4TF?ce{{!3C#p0z%DEqY8#8Ng{ zRxqx_Uj_y#qRUL`6J01W_ek)h|BDqV`oDJ48gzeyHj{Y_bU6w+xJb5G!T2 zA3Jm^$}(ru+Z@zjtz24jtYKA*a}MB2#m02s5YIs4l5Qm$qFc z(gB@@QZ0vJg|)QU0bQ=@YD!n>(qZJfTA1a!k|aveRb3UiI$hu2bNYThzwaL%Zrxb3 zz4v}S50A(FK{tEg(RXeqKHfO4Jzcg#gRGMq3TRm{|8ENZk}9t|JRmiuSxW0DW+D}G z%hE~gCs&RXcHv!S{AnFS5>yM2h7@~{j7pd^DN#D_W^v{!qM7cHp&X%w8_E|C7uvb4 zyT-D-;U_?j77_0|AAR?f9iIiz`!;fLNy0kOmLdN1rn#dlceKp~dtim4%De7kn&^qK z-v}Rle~(M>PYF;}heOUzQaM*mcUD}(7rQwYpDIEU%!8+gS**@}!?5F9EpdH&_QDPe z`hbRNOqx6^bX>m=fH8+$CzjClS35PW!!U({|`dgTv3X_nm;Lnp` zCjl|91yU56tunINJK;E9H|>4I$Z)-M(|BCaM#g+FgUkdw?5!4qBv59#t?=H{Jwk*= zsYFw&fqhDu;Ml!d4TOc1#ib_v;7u{A;@;not;{aqyT~-O#829^02L;Ma&NRrPao)dfK(`H`W)IEafJF|#kj$)Qn?t@El z6e5#p=I^7HREBuuz(^Y@-#1qiYL0FXv7gu6-TXJ^1ne1G+)vn2F>4vK`laL`fkvu(}JXC3{iY)~F{WrYP)IkTg(xU5s*Sjq~2Xld` zXjjkc4#{_jG5YNqe?Nstanar6&LBDDPQKn$olP7a8~ z8`H9+DMyEda7QPhNX_vaE$dtYKIA_R;Qz9V^f(EpZP!gB-DW?z;pP5K?EHV$0q`{8 zl}-8jeMIcM34Yw8It=LzJgHb1!A$KGM6mF(K9})Z=1ca6I@iy&FX_Rv3X>a`JhF)L z5nM9=eTrBWK~vXKBvA6}pZXN!PS5z6D|eHTEf}^3F1F}QY zRw;C_nDskv&g4ytxlXTtoy~Gv5Oa-#-FIDA+?=SBWyb&HJQt*XoIji>LmqM3Rl5-D zOJ5cN=6U0({QCMu$$hfwCN!f$RQ__cC@Gn5y;|@CUz#ePtaD|%pmrer$!hy(Y}d3K z>RDnjO3V;I;#`llxM-*|+AW==MpzsVZDmqg+qvPt71KQ4Osu^_0np3VoHg-wHK$);D@6jjiYsy&gA9BQH zxChO>5CQ4cmnC9k-9NSaFk$k!Buqp`)V!S10x~+ncUg=KKJC5|^jR0edH9kxv%ms< zy=(@<(4W`RVP8r!YqCmubSM#038NoG-yKodL%9$EWwpKgn?UDG+N{*V&pL;W7v$yE z$Vp1?XkLA)xoWZiwp&=zBK|XgZjx~FMJV6D9u9uJ)*Tbe2w<~x+ux~> z3nue@%2oh#3K7BRkT%~^UteqC0|Ff6KjxjVZgMR!e3!rUZ?i-YaGmEnuhFqVHh+22e43`=^im-%e=ipcI zPWL+kprA&-7UF8hZUS^?Wa8&_Q&u9OD2AFOV5C+k+EK7r}qIZzYdc> zymXJYn}$rtQt!xNI%b`j8*!rs2E{*N8s5G8=(P;gKk0s&>!Q|^^W5I|39HF8JT81d zXl5u#v9Z~$%r6Z~K$4%V?e94fnwVOVx9`QnS2P0z9Vj^UDztxGYETxlANCEt+n$9t z??4g0_I)w=`uMDN$ag*oG4%&i%eGyOE;(dSITI&{eMw(KvXb|kV}%Bu8Z@`Wu>$;C zIKOU)7DH{A*yqk&kPZevb=CMt!Bgh`*f40usyQw~_j~3EtEk}8BLAz>eI2uGOVqX0 zVZevjDJMNm=G!;#yCPR5hMi_J`zOv$Pp?$qRaLUlhyfuH7J)9HxdH8^VN=MmFp!aX zw{=Hi%sg0Zi_8cO80^zH8hca=;&i>p`cxhK(eTF)@A7=|S9!+Xy;{5RRir_n!%inJ zQb5xx&Bwm1SF4Ul4da~Ni1*AStUSCBbk9kwp1`5<-2jw_k^xYJQ{&i+VF^K8&}vM| z6hI4vm18Qn3BKA)Nt>*{mP=2v?R-W%GTD8#SN6^@C771c%Xc)@_&W89r;k)SQcN(zVt}&$ z?p^!amM-k<3E89k8yaeGsm8bC#t`4CG&CdHLtnxmQNTvoTNZ{EECmJdA%~5Z8<$i> zJnOPwH6pxB85Fe@7Tk~~7t~Vl-XD}kR}{DqlZ@yYpV-%aK}wu^g5ySJ&^LC-@D6qO za`q9C8RuOaNLV=?_1AuhwI1TzYw&jWy#vMTjzOigbghL;4~^+etp}lD@H6qU936mW zkVA2Tb*CB<5}9~|g_+SsK$dt&*Rv=JM6Qi~nXt&SI-k>ho(+n`t2}V6K2V1$D$S-1 zRD1l>MUr9o9B9jtWRW!}h;-HCG+}5{|h9iaG1OR`` z|JP?5)Xdv2ud6r#LsGd6uIc~ZqXG;magf!sL5@soRrY`o^4rn(&GBox?Hc)J<>S5V z7nZQ57UR)_L*fnRW#FuLUl=ZB4Wni%LZN z2(b&46DG}G-M+BbisXj!=V z=@5TD%7!V3&o_m(UPecfb^{c`svvNfi+iU}4_6?Yu!+lXJZW>f$hnR9N@N= zYWZ{g>+Z;2Y=;<{pFu5|nH%W>q?o|Ygjw|(RObzQtWU9HT(YMaF?tifY5qrBx7&q5 zGhQ<7`)`B=(vHm_ttr^k^RLM6mMzJKXkbw6!?Gz?<2MTs14G982WkYNO1p4-#?KT0 zCpZ#>>=&Mj6Enl-@UmfTdooaQ)p3t_oZ@T*;t22 zV_&T{Z9~Fx0c|m9aQhxs8d~Q5=bj9j)gLq82ur)Hi?%=dN3r7ZiiD;gW~aaUhnS4n zvMVpV{!rb+&YbfZ>ILqgby`X}8kZwJz(99HM{bf6m$~njK$z15J|CoX39k75Vvnd} zum432GzfJEMT>c7JMX-CrXo zd*OVIcE#DLh?bj}cn^~c5G~<;;Kz+`*pT{Rimw<tBrFr{$Ujso06pl6QkjOaUc1xW zB=#7$ap8yTwL+0+4x;iPNk(WFntW0Pf`RlLR>0sjV*JOD#R04+Byir{+eC?pNovip zxh1`_KA7jB(xJ+yxcsPvuo%yd{_ur&*Y#;bYbtYv_0P3LeMIu7MjO^!zOQz-8lhhD z)w1^t9WOTEU~V{qbWl6a>H{f%e`ezU3nU6=M#&OyYiY1h{K>wj!84+sD zVkpR`AnsB`0q3UgZVEflb`;bSFQX_8SpX|8sMzV=6xs>h?}ecnu6sJU@FRKwurL(l z%bd0l#)VfSd|v@70gm#Cal%zLb_V#lO2g1fIg0F3>j|?Kw}1=@hPjdRUM|hi z9gN!m3&!;%bP-trx3FQL!y~vrSf$Mkp_agj2-Co!!fr^q8?;VjEeBG%S%TcwjWStZ z&xh`h1=D$0T7wf6Izs()E>J^;5pL*bneK;}{fAMtrMCi~+fx5PL5!svrUFZ1y$%^O zwTDWlCs)x;Gjl2T1KEu^ewLs(=9))DUPgZ8wqI#xHtYf}0$nXLOByQxp~oRT*6Kf} z7OrXKuX}Ze<)Nbg9=h|tX}7zfzGKg83aBlwgH)6MG}GTqwhpahT=`cLvLfe{(9Lh8 zB8_7y|4Md`qM87BkN_rGm`)SOu#M6`h^juEp)xAaXMuh*W9XHg@b4_^1O`&{@86$v zwerx9cl6SH`1!vn=)f=ajhv~_H_e;BCe>e!bA{jQ{yQqy_uT}&A0e38sa4jwjM7(< z0(i}*chxIRSI>Ff1^=}{?jf`Pqy9};gTXF`;mJ~b1>SrgJ?k_I#nkE$%DN&5{O*Eq*5`u98& z-vIJuv8mOh{`F)8#qeOw(mx3*LwI#vv3w#`%H$w*f~O<$m+F}qKoYYDHbRHQ&?y$4-*M`LD#6+NZ@UscwIQ z4Jj-zx>eDZX&b1iIKl-t_h;^zsO4PKUjBRyw)SKF z1AC}2H>#b3r@f)y&7e6GlcEH{J%W-wGs3D=+ez4!HaldURK>z8U&wx%eppR$94qzc zQ?4q7x_;>= z(r^F3RU|bSDw1loUHIir_Q{fCDx4sqL0M{=DD#R+8Af5Z|pRaxDez zzR@au-sAwE<_<%b8qt?wu~5<&=qM@XVB-zz(KDCG{^ZrpJ4fH(*g+Z8Xt}7%dDPoVD|~hI2XS46{#==wGLFf8#VB-bSW<+^<3!9w<}GlU@##mx>YLa`Y5T;3=EnQ|4!5W2bs`t10NC zO_#AFf8m>9IYoTTnHYb7O`C~}S!MXA?z@kWyj`rqIHjH`e=Xksntbjzuz{YQ_>DKV zbXXt@T$<7MqdlMy-jR6@?Fm`XpTp=EyvT;E9?u9da)1~4l!X(6YE%_V&w`+W#>q@^ z8fn{A0g}8k;5cch2C(`_hLi>VZbT_gfEfZ}P(eAxkrF7oq_KVc>2MlE19<+-yeyqW z_rs~S%9#+jleim_4QIJjc*ETr&J-}lk}%$YF&81p#iAV`n7<<1_Dcv_oFz3Nsp;fn zF>u^8Lzqw(CO5p0C5FPCyeF2vdVoK>yv0(jpSHs83Hf8tdRD2}<{2lNfKZ7Egee#> zT=$94krK^9(9;Y!S;45qN6mPilVgFswXO?85Jq!5<+X1dm2yphoS2gTw@$65^$Xc4 z3z#{hi2Q1jM*+o?9{3B2Etp8@EyMzN>Lrth!ecKF=#SD!zK_<*kY)-g3b_MS<@D** zD&&njD=kGUBEja=V5@5*DUm&Dig@LSvlPXuF@q`Zo_^Oim8>~?%E4?ezegt1K;>Tw z8I55V(UWU0DDb+1npoAc(K|V?*TV3{c>dgtrw>1h8p4bT#dc08`*Uj zD?HMAi+CyEMMF|qpcii`1BCVzmB<7(J9^{1*4kSicp^yKSBIS{Z9=o8`2C_6|5(hb zCco&1a40|>LpcX|KEP6v5~vfhwh&v66q_3gzX?3ULFc@WT%6(9=u5SjyB!+jhZ0K8dPHJ3~P?Da*B>Ye~`|R|e{^UN6)OZ#w5vMN-&TubGvg5wL>zH_8 zMnkECIO+}LLCg;SIsE4RIs=`ftPg3GKh$5Otf72J^UP3_ww9V`BW})$t4_X6L|W#; z9lze5b-oZrESWSDO7RUR=bjJu7yR*i!;)c|$-16jy|&mh5!vNJN0T@icyY4juhhP1 zrT)%+19+j4Rl65K(C>3WQUCd21#el&rpVjlqv05rC2jnLXY4Z~@}pzl!5tcviI*>F zbg+l1(Q-nAcl+xSTQgodx%@zbBY#?os6C;=s-ahgWcL1;%EyvJ=1+qbO>tqkn&926 zpnfD_MVcyO^@hUzqd)k)ciA`g?t+o3wX8z!J~PS}#GdOt%6Lt~Lw<38F3gDAHg73t zhzaL|w4S>x&IptnTRW*^yn*eUBv|QX&|`S<_UX_eV8RkY?E!N@uWbQ;W*GT5mVc`) zSza{^tcUr9Uz(w{J4_ivJT#xThIBv$eX;ai$% z#p_w&mLCnco4g}qtv(`<>tdTLNbBPEYrAmEOxSy`X9_B{0Pkai z#Q_Fx8?`lz3fPvX2=WeG(X6q06Q33XQ$bEZ(7v(Ox>xOY+_u0SXFv}i0x2w_FImkv z7}=)G*zn0VpJb&v#1)(cx9m-N4%xqeR!mt+=RUp_1>@gHGw2&ErG;iBL@MdYu64FX zLf2De_Q%_Evix?`f-NOXpiFoFTp>Ms*Uh-gsKa)YuiMh*8DaJDLTqcMm95&}5Vkzk zWEfL{-e{O|cRDvB^G#&7VI#u0jftHzQZuktDs|Y_B1U$U&CD7pby{o|6=9wGZjk-Y zdJ%>gcDa#Z;G-=a5N6Uae%+5L0Qk6Kg}%)rm*IYqG(%giO%~a(AVULfZfB6J;P7`6 zM>X3JJvEErE@)%MC1yrT0ZN5Di~w)y2e|#%BCWFhDC>P>MIV2G`9w{l%QL_6hO_@( zYCHp1)cTLg*suf|Ws~+pt?dg1){cCoW2ibE)VbKr=(dR(;VXz#m?!D3&WJ6~FAaw& z6Fr)2bpxMVp5+AKS{3q?m*q9{EB9+1Mm@Tm?IHGrywhS*XJg1na)k!qzv*P2Ic2we z<_LdbYFVZ6n`m1uqH5t@k#)jdyc&JaJeRP?&NQ?NvQ=-Os{Txdc_Xq2FV>HF%Lghb zeS~iaRkZ{9Bplw*u+P`W-wHO>@aN83P|m_d1@Z~tkP;;e*=xYF@&Y|rM$#a@*s{X6 z@Wv+#2r%r-h2A&JVf>6CTE;%Xx7EYbU;|?^B=9WO9RwzQM0h?;iW5g_pNm~!vC^exjP z*l@c9QKCCX&!j z2Sg~SYtzuHhZ~oFPXI@HIwd2rZ%?dBfS3=|6INz9RxJ9HiC4wJcRt-UMMog-)hB14 zycTNP_^$a);L*2t)CtV9g)<`HxU9tQtk*^o z=OKmHzuP&#m$=5bx%U3KTAb!?N-7Ss7N3d?f zM>A^RzIFb(?yW!J*RGscLz$#3J&uFX(WFHrsfB-uRU2_-va#Ufw8yEzwaK1DGTv2bfW)bWtM^^ZDAYb;gK z(wcg+n(QZZyIL5H?x0|@H?O2xE5 zY^m`@wf?=qW_+h@TluggU`S;&RC&ipYE-s2BPP4xx0GPWnQN7PP!N7kU8wY0?ACN< zerBObyf71de*7G*xH$1RRZ5)v2_HD8zGlWji|;zP>EYCR(Q%v;+WJcixm>c6>p1_q zDA8sm7qfoRy^|MN4a>6q;&%Nt16uSx_cR?Mov!LgcY-og!3YQ*ml`d~Hrb;aE^}*< z4t~D>BcL&TAYYN@b>$^kD2HTk!d-EKB&O4(B>u1O39M}s(&3g3F~1NHdpp%uwGU_@ zqwhQK{IR1_RiJwM#P~-A?5#_3IMbNn&!iR(&C0OZFa8*1Y;Bj@+b=itMvW82l|i0; z)!lD%X%^pA6?=vk!0UDF_mS#3*;3-$o8J}e*;)VnmY;LkZiC{2XiFykf_Q&Aw^Y2# zTS~Ci*uRVzjRM*BJ_maiYjegRER=_p6g%>@*BW2&lVrl)%AE*_201{8VEs+*E-81Z z3cwD@VkVOLwt(P)*_4K=k8ObLabRx{6QXsn=W9I^ojdJ!nTYwH$nsXo*NS4YD*k@o^si zgETr;&N35iE@YCN9EC>y^|~&P2rx>t)ZqjP!LSrkyL3yswE%!d3j{Ew_Xy98rc`Q> zrIA=^PEYGc9y!`7riEc{DMikqCqY(|Pn+>qZ4&mhjtMs>&jImjF+fgeGqzH$W-Ci_ z;BP}h*a?Q00I?unV3w^cHgzgILqR&_zS)YEo@}@`J$A45h+n)x=zG8Q+DLtW(jCLS zUHA*Whyk^JrHIrDsS=3aZ}_|-RrU_#>gaA_>b)qJTQK=etF^Z-`GL6_sy0`PX{YX* zGt!4Dvnd#=4V&Ih>As3Bz1Ss2vY&41mc*?+BJYQm%)H4Vjpz6@7Ffua10l$(pDS-m zhaT~koRS~b0|hRKonhDu6NP#(A}sdnz% z<{MJ&gcNT`Q6;fJN^dkQ$0^aadrw$PP$X$sjPWP8m0#yzqBGPkQHvb5>;!Rt!PAWB z!JZf=M~M#;BeCN?K>K^>enb3u3<70`?4Bv&H!)PT99xm0V1z%vMA9?i{;<9@yT0u} z_8+CIF?S63K+o7g$%B?IC$>*y*T%Q@9=x9&FPZ@5jB^efW@Ho$^GaK|?7ewP^uu>E ziVLKEHf-#=zkIr{bOy3pgM*f1#3wnhL?JBdtge&|+~3hqeY56xXbxbIF1t@Cx24@r za((>~eHvn0hrRTCyms-P_@9pz4hikYrNo0?F1=pqw@L9gq(+1bPGvm$lZSos5m16} zLoqNvqT>U`O<3nO%lH=mphrT<^W^r4V3EqIIVs6tn1T{AHWY$7Zm3DeI2X<$Z+wra$7Zii3erXq`%v&b-o`t? z(YMr#q6`Yz7k~&<7#pp2eMU1?TgshdOJCBA(#iRaR51qz`GFCaggzhSk6(eIzi#q0 zBk1lPHkPr}&2fzu3h65Jq&iI1eTi?ENQ|N6un0=H^m~P+TH&nd{c|WsY3iqwwOmID zVG%-^zqmVE9|Zx!GT;z}U}x6iM!XypOV~7XR7HfN6Pm{<42z5h=QS`O2@?n@FRuXa3KM z``e#WlRBf5Zv+(a z%t?ySR#5mE8V4EfVu&jvvJ2dcWZ#U?eXwMbb-)*^L|34~hNQ(vn)k>l2Zh5(<;>fL zIl|n&Dl28GaGHQD(~f{>WX2T#mSaTl5sHyzT8qwvL}<`WRX5GJ;~;BK%wU`%e?N{M zg?PCJYK;_XoQlERQ=72SF=~u`1n#KR-Fw;Xr zTrEMY{d%o&ymU)*{bJ)Z-?-^-7q1{Dah_9D`ADo}rwl@8X#KOIQvK_&kvaF*s_!P| z=;W2v+%nS+)ou#%PpA{Z{@u+i$~ncj)54~-%E)RMZq5Cm+U+ezNLI2fhg{!DiLq@7 zqhE2>)f?Z}p$TpA5q7ek(_fPm`0lU$DHiJ|eQ6d+%M1u!BI=0H!r|D{-)EJxXt%>l za%>Azu*Y_KKW}kgRNMp-@N!1Gg+))YU5=Qx?BV&d@vFK3OATvtCP{V=`h-RB&OzR; z@CgZf%;q4A1SI+z30qC@ ziuui-^Qahv${d{7vm9_>?&;h)xr}fbiVTN`?T2wW|AX96jAFu1i}~hl5<@gE@9`oe zWcj1c7RF(Zku#!aZXwhn81iEtGnk>7!KN)_03h5?LE<@VmAbZ*X0C{##y z+B`KbVZ1;%j^2lB^EfX28IY|Sd`FH48^@6J$o`3jRa6Uf!%J(vt87AQmk~N~&yz;L zQETmqp4B?UngpX&Gp=vX#VC+%u)65&pehU(dosMGM$Yqm1gDFi#?5?w@1<651CGp{ zRxlHg!@SPk5}iel27L;w_Q_@;KqCla@Gfie$O-(}CkHB;u>9%UVa-%!Q^!p%x~xWlv6~=wN#JUnLWD0)P&Yurmwz%`e&MQ6`AO;QOapTZ)e-+DKN(Z*MF+8@vU#8GVgs#4YjX zjv3p;J>G1kVbm?#B_pIC*7dXns-rFm>sx8$J_ zyJ|C2fYEWeFtr?5cEzd!eM=f@Q>^=XlW(idzax932HgNmrRZTNxZxv}&kL=Qg2jnn zd3P*FHbQ!cuiy*$an8UsS-cp-%kEgrvBX?EjAIGRX1XjA5{HO;q9$PBJ9i!D)>Cg zmdw!Y+u`LF5Q4%DD)u3a2Woq7n?m-=D)i7HWCca4+tVyJ^I>6wGU?um*XW!Lpgg`P z@F^xNzl+Zd{hNewzDF?1stKBFDO&~cx6V>x+SqbXuqSDED7L)_A>7MGk-y-&P95$O zUfiKZ=-%=-vELsKX6)YV3NKDz-8*hJtQc^?!tA==A-JN0f>}&MPuAy#%Z*_x);O2{1_+9;qWgeNSr|J)j3lnB;hVc}ZiFi}_OW1HtzRb1cx0E0LxMm5)iX51`@()69`(w3`{MWu>bNwPXH)>$J_`2t?9W%1|Umi z;x?2DQ08-k;~dxv2omt$6lkyt2hAD{Ga9Q*5dKX*N;Cg2CtEKo6F(yBpp8_=suxjq z=mWc_RKFsmD3FR4ay`U|L6jd68G=1S)@Y=%SY&kWKeBY#IUq7HZOw#Pg9tNsVo0_= zBy_?J)}i3)n*U944nvBmXAukY{$)?v++rcKeJYy^p4B%@FZ`ur&H^SGG!;Md3=0eH z$QE&IYJW?ah-O#oOq1ZS?qc}I9N|Glz~gk$TFoVG&E$*_nM%7W`g5JL9}jUG^ns^soy+CX$aL!H>ff$ znikr3e3c{wFXVG!$WBvT)e-Xq{F+s*$^!iT{oMZC-rTZs!MgiK8Y>9-ejNHK-&LEp z7R8Xugv)sI#MNf7MY=@ZZ4E3;008W?`#$8JcMadbe{{yhC?{{_5*&(r{mjF>UqSic zWqrDuzo-C{5&E~=!mbSZ!WFxsxtXyeumfCE;kQ$Er>@gQLRHLm##^s94dr4egu}XR zop$4Y^;o{8tvODAC^*2&HKxc*c-Y&bcE99uS5zX#-n{*fWqtV2S(8S(Ov0S)@{%4J zpgMFxy?mej`PV#`!t&CXN8!y~vu&!T!`HB>dPsOMGELhI-Yiuamu% zMP{lLO74+ZF*hgp{QJVB%%Go!V;4M^ugI1z^cHCU)G(Yx1RcCn>GJq=&aE$~gTk;k9=eG@N2j&&A^%b$5@D8+~#?3=*|? z-xzL%-wqjoV`;Mw_t!Z;5oxXdc@;UZKBO~{Hd_bC<o-#RcmlrW;A+O3zQoqMM8{xAO3~Lz5pu=$-n5W z6F4-#UO5|KBIa9q=Fhw8C`r%8)(UMlb>X@WG5y^V^L*}eZaDQ-b1^QoGc83fOG$o( zpaIHc>@y7kW&_Z(4kA>=Ys#|~i%s3Xk}$0wWTyAb8ENLAEwXaaGad?eDwO_QVjirD zN<7M$p^v@mpIS1`&2vR|OO5-VZi-L3y~1l_E>2JwCK=&72UoiVf@5OA=j$y|{@0nt zARHqGm(#DL(41$1h}4O)iMS5hD{i24*vi&z`JK6k5Z@NxzH$@-!t!D!YLx5S&e9L$ z_!q@E+{}X+XqMGI4lyGtl)w%2y#k{&0Zqof+7lhKmx-OM$ps*UAhuU%M{?w2L&3sk z7D|qL4od+xFydCnjAg`j-ab%-OdqV%gcn+Gast&&0)MWcKh#oULFM|kwotokr0P1~?v_M{|Mz_(Lw3II zz_T;p6#3)zNa1?{PrM(YfCxf_c|$XL&gpDi)t|62x_R&4YTQoGNGgO8$1x>*g4?kmOdkyGLf3UDIo(@m+# z#Y?kon1tl!3OU_SL%%hd2O?g+br>b3ko`AkH7r~XF@$zpa}>FmCq1?7om;npadjAw zAM#e{n`~0$*ecRTV=3{4r+xj0($+c$ zh)Ldr1wD8|g?S7P1uDfY?Uz>e+gxBdC3WF9UE0UJTDhzKDx2|^HpA0a=@t7iW%as1 zR@lb4wJj9fllfu2%8rS;L94)5tYJsKwef_sn)?JKi~qXONG=uP?Tx-$k{x5krK!waGrQI3qDe$pgg#l*E9BhTvXq4rvFi=%E z!xXMm4P~)tRv;zcPKESnmKW-7S=K$E?I&U5o`gb}V7)g;>wu*j0Wz@^0nG#YeCrVG zDX3+QLYCuqD7$G!{-fa3#&4E%;i(_h;g`jm_v-k&A<~MZUV?&<0S0F|jA z`f-8?95fuzJWQeU^wo?0m+-Sb?)YeP2Cz+a0FxB~5>dY&*IVqsmINkq;EclsiB=~} zqMtly(|w9PUb5Zz2|9({94^Accc!Xa% z6@s%|Rix7IVmJ?DY9PH#*cc!L)h!<SPaJ z!IxUDDo^DxNSIvzI`S*fUe3tgk)2h~@$15`B`-^r1K9&RJ8_3BUc>lGX1~s*O^>|F zsy(_rleKPnVDzO8(A}o>EwjFGNkozopBc&ecsCNYTcw*2JJDa(mG;s_$V$)^J@?uq z;3VLKFr$GM0ezeDiwTH(hE{l*SH;c^EY20C7TsTdg5&`h|F;98>25I#S(Yy_@{1uP zLB;oodzZHotZ=7ZAg#4ci%lI2_>Se`s_$n!7SxgeQaRetZ@<}A}>r4Wrtk4klIA)_(%-u z-&wbIWE+pL@yCRqeQvk2n25r0xXEvc9CyKi#|*5yqb8a)T7C?n11UWD1bi=oj7HhxN~6{A z-G#45>DieOo25ovvZ2l$BsWVjS&$7fw^&t2;vp&9l`bc@=0_**swSUL_oU6TQ(1I? zoqfLwX~z}mlq0SFbsMWyGazHeN`|ZE#wCdp^{SylR=Jue?Lw}Z?4op7@{v*2^st;r zESz*BwKv$MfFnZ0$dRpF@A^ut2Xn^f0k^$-Ti}+SUTctlrRx?tePrO6<}5V32r`AYs=v~2(B zLx{8Uuy{X@GMEvgK|(w?ZR&9tlVA@kP)XR3`k8Wg9(m9ySs4^Pds0Zit zo;gjp1P9v2z3rOl=LpepLxumC@1bO4Y^6!rdYzCF@h&IMRJfG8_4?**8)mgE_Z{*_S3-GK^NnurQl5QX zveSM#=(&h{axV{5_RmK`eEH1{%?k%~lgS!E&QBhZ#?!ZA<&T8qt64+25aKMOKqUtt zB7hvutY1j4zo2pOn?XY-#ig$HgW87FU}kvHb30Y*uU7q@F_!mCgSVB=3lM$q+pHDh z3#X6Qed!Amx=+gD2>_fqO7gSL@Mbd)%0FKy<6w>MIR_lyVbjQp`L%TQ^t@|b*gPK= zYj8#>9gaLRH{X&gEQ}^BL(5;Fp8V(R4{^g(;r$CVGsj#y$psjOY>o}AiMS47lxp5k z_2?CPL&0)SFwQuo2l8Bi{3wabin zxEbHB7-j?4b<87?eOY<6tXLK4W`+cp(#Jo(9xgf^f6=k?r#KajIiW4`zkESYSffj?k+(!GAdjr06P#k+h{;+s_2F>`fqfjEw zk?gdve0Ef7nBSSO-+vS_tdkGRk;OfXrR0O9t?~}3;jGXn{XM&rg~LEnjx0$?+$;~t zXDkIx$|G~i!D8KES6Is>YM%p+#is~%`D7GsC0FD!e6^_|Xqj~^CY->i zeyv~6traIEEn2h;TmY_*&g8^8oB<4x`o9|divKgBpRxS0Qt-pRPwCQ>IhFtr`U7{dO)5wj2QF znUA>UK)5Kxa_h5DY~dt#->KYWHg1%sSLExDNIl>cfH_iZeh7-0TP%7TwcRsjJxnJ4 zJ0SSqL4m%`wfcM$JPR(L@56fG|G4+yb364=5%j1j)=aHDsC_a|4rUv$+f7A57Zr_E#KBQmS9qfWX*CZp>oA@HJsYe3B4QjkGzW|Rij1&a1wQBR5= z7^unw41>krOacsjpo)wBk%eOXQbt;OvpgkAYFvI?<5Q_b*cZj{g+QRk)kB>D*#+7( z7&HN^Q4a(Hdn=}FhtkzJQrL6!CX?`8@$<9H^y6_~-{f4L?h7(PmNje-E{2Eq>N9`f z1`SV#gyDI+1@Y#%?nD?$K>`o5=lMhrP>2$P14q97d_dvTdrp%pz#g))9|A=C+*S-* zECHlm>Qwk&?trk7+6DL25SIb2j)-iGCgXQ5=Bf~lM7zaVr&3|eeha!bq-=`{$`*EE z&i62EGT0w#QIm!gs@#ZouZv6kAT@I520AMG$jjc+rkC9wsc?ZhB(x7n&8npg9TfZh zf^77{KjL*4OPiw_X$7sm;)|v6PoBno&XRIKd~TiA^D?w&jAQv#ceTM?*~@Pf;~8@W z>n6;i$;K4I(fs$({cgWfu22ZakdX>6qx}+=mG=<;f;ZIyNH+;FjV@n$w(s85y>ZQX zl~QnI8F@GB{2D+C-z*wmv(WEtc+`@J=Gdh3D%Nb2|GW{iQO7ibH;`qgy8>Dp40`(I zY^ALy^YT(0dzX~9wvW#x!D3UKtypW5HryDV6R_=O*`Ef5)=6uM3XGiO!M^X?8K-}L?%yDJa{P`z zc&9=OpL=D*)ireA!49{$-OWkm&lmQTJW@2bvR|?=m0w?A&tluf)h-Y1dR{UwZpmm* zs9xx^iHZCDsW$3F;v)NdW9c^U=|odeMyY0nW^|QH;jBA#k$!B1+dOpIj`7-6+>qIf zS?y$10u~4B(j$-Pf%n1^exSCQCm`8W>FxlXI{nrtOW8MM_yCsRsFOjEzuPS{o&{A{J&swXH3Z>YXJU`29QcK5#7iid5pjod z^~g|RKFzRKe0vwF$OU0X@4cmD$bqNuBj1MD#OA^#$Tj38Bg7v3HAxB8wf?7SQ<<;zHA2x8RVQ;AEPzhA-~dn(+d)xfY}3sx7RvZH%Od@YvS@<5z*a1JoRMCM+smC=R1% z4k3&5cN`~Sx_Jw|gAmz>{ZN3(Y&}So+ z9eZL!LrJo!06hmTNdx2siil;6kTmlhAPqoU7NDr?%#Az>L)B?uvBz1Q@uoJ%NY`?K5!L&v_Kux2(&-^B(ufKXrd@Jlq1~DL<-a@uwr^jAcOF zxbe2PBI_0N6{tU6(binJ#(HD1)}^T3k2!ncYc$Owv5eFoC|&p+0Ak!OjcDfHkuF@L z30c8Qdmkw(Ta~*;RS6acp6PScm^5v+FDtp`y}OV0XeQ%EGGq>Kt_rK7t=UiJ`Hx>u zg63Ng8G8*N*~gINJaQm<%{s`+{%&ZM5paq5}7r79^OTX4oUHu=yNE1?$(Hv$#h4nH5rvLv0&RqPoB#Pdh$K=N>(Jhpg`Zuh)&Ca%X) zxni1_XF_y-76ZNbyP?MV!Fb9D(|_!l`)px1(Cretp=z8e?B>9e3xx^9!6#86mi_G( zz-Wza1qs-a=z{VA{o746|2=;5JogW09jlh6dE1GQ(7=QoiH>>d_x3`l~zs zxg-2Vzj@p^v((*F?fn19dK0jw&UR~fClDsV0fRCrlR`p8L4phd29YTc$N*}owF;Id zX+;r76dRmPnTZM!H9``ImbSL2SjD0?PA!TdTE!WZidL-(wQ6nu^#t4ZeAjp6v+mX6{-0bmtZq8TCOC8ESpCtS6_@*6; znDY2=_s&GSnug`NZ~xM`&J#_W(DvrC;Z@8T-ad8nfF$1}pf`l?%MY3rvO*g>ZZ!tj z&iS>M>!vMFI5_=_gU?F+b9{2aYWRox(oS~%%z9z<%dkbUGp()U)IQe7j67~slEyFE zL>t=vb#0#T@Ek?WIqzzpC`>1O^1sRB?_n&FUK$#t`uNgGD13Y|^?5|b?tI~CH7op% z2~GD@D|w=Nq;c0etQI^I-00tL9a+3|-;3ug`uMQ6{kF!s!}l+yZVT&ycR{o=Ygbp1 zfg10^Arrvf^5?+#etWr3?A|&j*T=W<>}n*yY2Eu}XSCu&VUMpgtygf_E*HZZ#ZNt& zl8@dCZ>)D=(xwFQS`e7UNeNa&sM55VNqxdZ9leYxk2Rox;_QOO0H;RRs~k{Ud9SW(*e5nm1M9oxn3uHRl&3JB?{i*1xE_D(|Y5OsG|FN|WXGMlMy`1}A-AbFi#nN|xLz@>#;q zt2+|f6$GrmC`=vluk2SddLHa~iTRl@=Kwv77zUehRU5XnZWrmQHgwQS;g{Ljl*k$} z3yrZb#qv6|HXGGeziHS2A$_jJ-$+*qEI&wVvRI+lv(z^2tgHy3A8*N5%#pX`BUg=y zbL+dR@n=ytrW&7&ZN918$fEO4T`4Zh{9=B+RBc-{w%K)aMe=sYSA~rIgSqn@s`99~ z#9>ah4Vyqqj0;_Cwzj*$%iw$LaG3mfNJ z(mP>gU!q%WZ}n)JXXpA1VL;-yFA|5i)K$BWxT18mfr50}{6k`oe ztApmzpg*VowllZ(AKABJ+R6@#-(F{Yb$RB`&t{UU0_!$Ns_4#%!KN8TM-4)A)6Pm= zAgfg=rVW&E9g7S^h1{x}fD5OXSI=@m(L_og{n-m zErbe|XuHVt!sXj-JLtQhuIc5YNJ-a-;81ftX6$9mj3pz6vQEq95RY>o3nyfoS_mqJ z&zf+&pW2#w?be<{I~H1azXz2{h4L#R8NWcZuvgP1<&nP)mkDZ(-vW^Hi3(0$|0vDE zhx54E=8J$wye}{Np5VX7wV7>v?}cLAnexeJ0H;y6 zO+6vGR}pzauo2q1LmXeNE9tJgo(w>E{4Lfk|A1qc)^lf92Wro@&vH9zyw}6AJEf$t zLngIyhjEUUcpeDyVQ#a{POehWe>~EySiibJwAEqMkddy!PSxh0)k)mhwW8tv(Lg-q>x?HhsK@Lo9F}HDFfs zRPe&EjjC%osZt(_Fy*4Qa*a9}z|r z#xdn7`N_qd5(k(d? zw-x9u46GEf+-O(`Au1Iacc1}OmQtAxK}VQWf`P}MQLiW$Q#T}#Z-6p#PYoxlZOSLnqIrK;0}0-rLTr;VKnS- zpC!)DsykKCk^VTc!>g_6T>Z~ul?E3*y(8%8xd*IQ`aiX}zZd74$6juSc7UctBd(zF zYX>651qU$LG%I2L+rh;eE2JN2!F1{ukx3%ZMCPXL#Dfu3*`;-IX04s1xBRa&UAwK| zs<4CbDLKx%CWt;@vtfzg?%gL*t%bT`m4mGgKznS&}$k1ftyd+L!yO zd+Av>6AE@fD${C^f0e{_$WFNs7yg#5p1+t~9m~P+n z4?F%$@1{){Et1e>=1x=rG{6BOKno)(%T-oZ3a->dWx(cEbtuhAbVuNg5mBvK1&5JirwS2*(+rGLnEN3`YnZ;b7J_I_qqf#VNSVoi+ z%^q^CEz>7|{-sc?DEfy2y1NJMCHiBn!BdMW(o%G(3mknXNOV)aLGkRHQdB2d+vQ*t zV1Fs`i|3u%d!hy&CG2D}8eC&l)bz;``mQc;0B=d&>(W>#Kt>2;2c_!nN+9K0{)hCbDhRHXU@yd+pOuCs1E}Obgc^ zsKC&}a<#;Jk3x!gX@L66bsmG7G5m>cN``#@Y^ThOSHD2ZV$N=VmracK1=PiYuR}pm zxN2&2iVn15PyefKw^bYl$C?jMYYh}S+;duc8Gl`Q6BV5InCG&QmARZnJK9d`yB@;($=h-DzZ9bEEYL+R}UMe z`tW3(!Gc4Z66fONj`MB$<>|#3W1>2>FNtqBj91=ZX*yI%{ zUZI#%YND3k>$GW)C@${6K5Cz>mPxfrSx*x%-$?E%e+IYlpRVk?S@`fkt^4KoFMydh z&fCLADm^lirb3Oi~uznej2*J*amj{B0 za*3~ouMo2az;Pt`Y#tyr@dFktKsce|k@AU_#GCR7< zCSzn^T*TZ6odfprXDPSQ-eXhqc-1Pkb$78i^q_Aq{<724S@V;~5o#Xi{ZqHuSk*Et zJSrYRSuUkZavh-!mJUZ*S6Q^{hOr-qO0?IxRgK zUeRDE_WIHxO#4ujb&PgRO8rC6%_InBR>i4$YR+Gy4KqBQk<5`4(Z ztd*RjK!lk0C`~uXkMkPc(Eq{%5$ddn5dDA&7ZK$m`N0Z1EpA%o6^L{gLdTc*Wv*1i z%Med+X1RFO9_%Wzxlglz7P)X+? zML)$HY60Is-=50w)CINGiq_rio28w~+z^^?7Yfd$P+=Lm%&oO=xx!9i1se&|LR8H3 zHq&TlNqh3PNY_v6?!4d@_y4-5pmlUi`dRvPYq~aRielazKYE>?Z~Y6})6}%;XNk5| zie-To$?X;%P}RX%H5H%00pw3e?3KIyY8M+7vLn(op}|<>*!)WK;(%Z+Q`y$+*Vet` zv9)nuuGXQQw&dQD9iL@2IEhT_PB%HSoFB5WZYB?om!NqNB3=8krm@a7WPG}HF^fKA z^Nl>w^-19&v!-5<^?wA<+bmy=diABm`DD6sQh}M(CtBk2%w{ji33eO)_z=Z2tJku z5RSa?t@4qaO}i8}YO6mQyPpo|JvMO8!gRy2R4F$?QSggoK0JkKrxlVO)pE&j^X3!w zg%45zoPM?Iv<)4WNiR!y{;a>B={Hj2=>QeYX6IO&6Hrp=;E7{64Z-fDQ(y?~AL=9S z$Q4@k0(@v~J+7%MBLIUv8+536^nnq&@xrj4FrJ#6YkF{odG33Qry{?b*nx0XV=07} zB0>Tk!?w{{mG;{&q{NL+xfEz)Dwuz6G*~}TrF5+AQ`_u)b6VFp|K6S^KCTRUf<(=A zoIcA3Y&M@Rr`S_E(av`(L@51EwZ^jEcANg}Rad>0b$E-S=lbt`b?Ylf`pAsYH7!uP zqV9<0T8o2}Ake!C3725db3iyTcs024sD%NfZiJSkEbO8ar~Y1J&qtVop5bKGGT3<5 zgW|%P+$6MWO=RaYR7-(PzF~S8G*Ukc4_npZW_4asEN8RmzHhGL>6-r!^}YztCaPgg zesU@JQA^lI7t6R1FQx)TBe35Z#C3Qt>Co;%b_1-cm|3;V0ky(1M445+Qa@A{dcj!8 zqFHl(>l}#%SMGS0@4DueZn<04qxc?`fIZ~}*Q$qGZhTCn9FLZ`ovo{4w%KpidRFc_ zq?#!o{c~93f@z^WVn#dnAL?@^&3}sjhXanoWNSh3zjVARceU}c+M;mm$JnRuusE#X z|EYNoxXlpbRtDy)%k#K~n@Qi-GP9XCt0RAB4KH27mUq|wDLkPomC(6=Lf&37)--~9 z#)W#yEiW)KoV9jS71T$8Ba)6Q8~f`~09w6Tx8vu2=Ht2&oW`FySF)cHrzU#fuYIW( zq}THkd+0^B0CIXa!0m-fXrJgdM#gQ{i0GcKyV|D_S(V|E6AkFVX`|e(>`<)&u<3HB zSWKE2DZH$-Ye#KRTP&Q|wTwyKY+lHG7|ey7eY2<+Dl&F~X84^99{t1pd7ln2sr|-X zDNOQk{)e-mXd_q)iZ)y9d(3TqUBW<;LX-|PE9e!0Yi}+tuSGwO*oosh-H%r|Ia>KT z_TPfe{596Ny#k`~Q}7S}DdIydq1jxmWAi|hZ}9Ih;~1ixirVFxdjo3fuy%ZyJ%sI; z&Fgk?8E}cG?l+h|6$!i#VZ=Xgu%aC#42-whUXFdl{szCBx*vE;KVNPZE;6T+B(vHO z{@dnFB&TdO2lbKzE$Qosln&-PC}aP7r?Etlh&_3n3(RTpokTDY6Jz6GVlv}3W~KtZ zDV)Wu6bxgYAD6i-D2@3narz}hz4drc7QkqrHrN@tzJ1p61#fQp_p(ciHgjx#9=HA6 zQ<2&-pTBK+Vtm-+>4$Jmjw}1KifP&s}1!fRqs^E@Nh%*CMHrik8RAux2?zdTb*Qd>0_0_8Iy)Q>rN~q4^|4`#9 zDsI(o|8ci;@6Ic_((K(57yr0#@{*f7FKzo?e1LoQo7Kh_B2_M%x=|~vN`n=kMW_c4 z|F;r($9YcozBvazqs=C^v^k>t=vWs9_VMP zbtOMN$~bHA`xzzxO{cEx@VT+VU z3BG>a$sHxpfNygN(NjQjr-G*LFeg4jXBjc4@X~;Oq;~S6yRR`vr6v^&rq@jL^k+C2 zjS*H%Ea|z&v)||x!)|hD?(R>b2LfVj+u$Jqw`z1nql!^a(M287BPdPdd|~~>CkZyz z@AW0|G6cyeDq`lzjFnb>AP&~vKk%4;*fKnN-H-=9#ULBFNU`s)LnvA^vpl6 zYz+@Ae4!sKa~$YKtbf=HYs&`^j>D|N|S`qBoI)VK#*Wy~9B0R}j`ff z5!0kn27+UA*WmD+{q0!xfO~7gl_ERbjZ3MT{}gA;L6WSE=NqUY=B@F;xQ~(w?ob=L z<&LRAJR5rT8DFTT+}|u)WNH$h!( zm1NxIaYJ{`xGnlI6|I1)_rHL|U@k#OA8|HVWMPPZ8<*J1AcV93!9L{21NnEQ6sZUO z`x|SQ_-FJDLL20Ol2<&QBLMpF^AU9nLl}<`vkE#5LAbF*zsHN%Xmtch+NWUZZTy6j zh0Hovvff^`|FH3^lp9%lx5ij2vb-*YJNdVtaA=S7Wc`?hrayVe{1;DWl{I~0FwKJKxa6-P~_yf0t@%w^{Qfx*|sCYW_NH zYR~=#c&XDi((r`!uTWck#=N<%!Qg9MKz)^|s`h+8)gF|6y0cF77CY&`8sC9z+Spw+GB{xBIXM{j;&R=9sWW;PqPn6DOi^^>!K7Bxvl zCB@II^fiH3v{GP)7R`AXCb-I0?j_P0xPa<^eI z;`E@g{t(;;W|w0Q`Z=(jiW@)Jub3GY)|o{|%1uA^sN`59w2glCT6DeehABAcR^H5j zf8fPtX%gq-^JcPxcLlv-A1K4Tunz8J3M!cRC0pmn zrDzjgB&nnrnptnJUzUJI^-ZJSdVa%l4h@Ws_r+OzKRV7&b)bNy2x~zja%G>YKrzb; zPLu`{p=5$PRuUO*lQ_Mmu{TmcDgQS82vuitk)~r1yovPh5?V8HLEs_#qGb#b?AhJ) z!;O%%zcU4lme=XkjrDI1f=iH>YzQTLH8cZawwfYW7-<*%WS0VKSzfuBnz^zmZw9!1aRJClzuc~=bY0QW7`cer4R7Jc?AhFGdurs|E zM+lqinEFcE@x^K<;)UZ_3G?Ivs65F;h;QZCBRI*$bzmkw#)+ z2ONCm33z2rE7*-ANJ?j1Z}#S<2o)ChHlk1<_%X%4Bc8d3>-3=UrSYlGs~@mhD3U#! z23Kd~y5Og{z?&dK;{4VjB6^hSE&CjNjr(!*%_uT{2%1&_y#q(Z=60igv^J>k5i~E6 z6U=21$)ju(Hg>T{0rmyk);^wbGeMi!isO#=!Xl0Re+nn&THYl>RJv|!m}U$ksB#jn zfwa4v{GxRz_MDq>Er;rkGUlj+f*ZRIjO1>)@vAn;?vCiR{Io-&nl-*8(Cf$+C%;y~ z{?WlZUd<}XctFp%WA}qaZdu>^ecR{NS7i?0oA|rLGFp+zw$Ekb)0^kH^vSnFtQ*w> z>H(c)+g-_H{zbY{MAfY)UO}wKK=vK?uAQrUDrot!b}W=PKKq?z@Qf48@SK?N`KmJoNV{Qt?uqrA7EtuG8gem8iYz%L(&DzgfVQbKZ~3 z_G+UCKKW^kA^t%S*J<*K99`Lj{I2>ncB)^jsz+KF7$spwT4L6MZh5~(j5fd_{^7@# zIdYGdEM=e}p5d&XQS(xJw(*Gegn`1l8~%)+tJE9iN%-EIH^< zA>U8J4*CBLixniiY)_Rj|Dk4Mbb%N&$T$OVh;MrL!c*W?^|Vp{CVld-TLC--yk63Y zn}6oue*s*RG?Q$PC&tbN(*md|la7jV>dUlA|L1AJV}~dMNLZX``!rrUwZmekcGjZf zvYC+?E9STK%6;-H3uUHHY@)XbmJCP>vLTMp4l+386OvRs9yamvG50VBDhgcmxG6A1 zx_0p=g-;*9xy2q0s|jLJfw=O zl#g!mVuE1cwjPBy*mbDU$3ripKp{MnxiiGRK}Dr!Rxy8!OjlAjFl-_58XuEKt`6uL z;Zcng2C|(u#K(Tfq}GX_UtZu+aC9;&)nD_`BNqKU+Yp&>n`5^adaQ8)#6hYGIZru) zv^D^|f|{2%03vXVE&wun@%7K7&sU&>bZ*{6bpC{K&|oH33gf*RvVa*#8+SLHak72W zZzDwX1CT@i(MTd1r+>Cq zRtcn3^Y7vYehO5PZ;&D$#s`!6DEzN1;!ROuWHa=J3IoU)Da2op}{xzsZJN2OgdQ@UWdP;uU%x?6RF7E&raA7>clXZ0@^@XYTUj$skdXro;W0lkx-H2NY9Eq}-GoyU8+( z@ZW;Cqn`T@sdTYvbiGqLFfuW1j*8lq<9}4f9ycKmqvIQAu~BJFXq7GOMFPGb=%%>G^zsfq>V?S>s_et>4QO)RnDY>< zPH9UJzIeGsODr zzRZry__TQc#9;+g?0Z9xXkWDMo41cUPBi%|v`F#3(c8QoPmL4kAE#XiqZgHIsXE$K zD`gJ!Fn@zPdn`Y-x%W&AKjs&#Q$owU3qJKj;#njk+XW>KagsQyK$=X8tO-5wB5fa1fGa_4`jM>L44wr^+tehRKu?4ui@ zTM(h(pTxSikr>`mz-qyo;9)Z5_}Jr)W7~zTW&pG@O|rh;kLJnW_hkT)XDXhktAlW=?EN( z!6nI2JZb@!rYh%NZJ5NP7L&0FeLFf2yG&I2|2E`~72k{XO-8wQs!r?VxGW@q3P5&g zA@%8c1X&iw9C%xp{*O#ndx;<^qt{_lY8@;N#io2GOyYv`GP*s2 zIDI8kXl?2t$;MrLRI=tIN=Pg4ZAZ^E9)qEj4vB_iv!%E-k-h#Y#&smz+Ve;)3AtTx z(ee2o$7nSiYI6h9!8LtbcRiIGrlvs^mVif0j5zIi4gpLXSd<$AP+4W|!Uf{R_wl4S zSekJWkr-;-* zuyPvl0hovTXUtJ0MGELuRmq8rsjXC$wEuF0z`F^o(08RcsuPm@P|wgx!?a~?EBF50 zS2yV>Hcyck%^96V3WXaBE;S=PI{34$gY@UKNGW=JM>sxeE#BW}(arN-cRnys!M#i? z&R?P|`N+KbRa%1^G?b3-SX?jBurPzMv(-H|c*Xt#hl&`S#4QO|XU2#p7!r2H6@fVV5YgN}KrCJP2u{B}(Z`($>a`~PFOHJ@D+K9bJ z%1hVys&5rmz=np*eV7&y*vg-syUl)!`@9au>Xj=>+Qk*cj0gVi5x=vKeD<#c5il z&sg)W(9`E#XL)fUTHw-ZqyGj7H|B)!2yzG(v<$}!)Um^$!7RtUeZVqUqM)UT%9V1&e6 zm~AS(r`lM?v03_=#RAini<>XVEGaCrf&+sIT5? zX6Bk`+1@25_sjoob}FC_91~V#eDkOFrw+?EdRfaS{q?IY&T?oIj&)Yq`>a5;Mcf?e)WwkIOxhg-@7w`YE02ro+UC&g*n? zpUrx`ys?K`p`Xm6K@t`p0q}aw*yg>!p(A!Ea)884Q-qn-Jzoc>w7WIZ?(aLM+2J%% zW;^Txi=M&x%_Nh)B7+f4yE8#!6w^sg3^KP~G@XiQb^5p8+CDUwk$YfxWB^DG^7*F9?dy$2!@s@_w$Ki}9v}#wIMR&(t2T33FtxR#w0LMb zjrGD8>Fvh4F%nvvs2j+LZ9KX`%l+O=chL?FW@eg&&6n3u7nv)crU zua;hJ|ElrTz7(V$@2*1W^3|qlkh||IK3um4t011*F1&pU_jF!#wg{e-%Q&?0q_6sK zd`rfw!m?ITdiGOS*vd0-MS2Mm2IP*$igI9L^~TW9>A2@z&x`#(3w|GuAKFB@_|`H& zgP`hLnGlK^wY@?r88Np~5sUV&C@?QFNh#7rIGJ z0*fiDjl<)T_%f^M*EH0xid7BqQ#+lN_dgT7WfiY!-Y}8xh5QCe>Gt;U4^(!FTsZ6< zX=k<{c^Yq=Y8;LecuHXVlsl|$XI0Yoa;9-__s-hR9mb-vb|S7mij2qLSOFf#po?1? zbHkndR1O)IxgF0=v+iYHwphVi9-WtB3lnsZtXPkE3^RRSKm$6Ubtg5Xf>K*Z9x0@8 z0Bek4YMVMi68J7*x(2c0{h`A<7^Cl>TyL1Wb@^$AaAWd?{LI;HAXzI?LurpvL+ncq zoDx{%ra8aVXJutsYcm+SwegHO!VMNiIdgTV`!iAc!W|3i4`)&teaVD7?6By2sh4x+ z(ml&7BdEgh0ak;UEUKu7L0sD;RVL4B3~UqHaiD@yV2f78Jcsw-Sv;R@+WImcx+bTX zkQug3Jm!iGP%838lKF=k!l0!C+V+UlC^jl0glJZn?#3*o zaRn$fSwIVO*;9Ho_0EtOeHAyoMrz>az06NJD?wwRdVthEm`b;S#$Ao@Tsp3MZ_%1F(d)kQr&u)Z**?B)m59cp)OIUAZ95|? z{DJpD(Fxxg7ytOx9gJdOa@RVR^Dey&=MIvsYZ$X$6$D>t>a>J|zE~KCONUZFKMia( z39WH5BatyB$S_SyjSmRGB(x%SX@d}EIn6D|2dbfb@O0c1DdIkO!rw9F?Up6n=fsVPv81p`zRrkShE0Tw8=)kaBmXcEM6a981|JEfp>uB_ZWR2FT&3DJwC zMMGc@CE0e2Gh)zX2Qz}UNvrAU>q$pk@3OyiLV+T_q@I9yDs63v+(SGYF=LA`5@j8 zE3AsXh-VQ|hd?%uKuA-JJqO3&t%c@DSmw#ohB0jWY}Mxlnbdx7s%H#)MsiPEAMXRDOM+H!`o*&>p=VzXlwy5p?eqZzaqO5t8JWxXaW@cafm;%MO)=}(Kn?6XQ zgy!+ae)*gALC%j@tsPs^-$XqY9z&*n+rRfaft=6t@hp^3x1T2Ut~RbpSW&Q|GKxjp zl$jRzI(@g`B2Vb-EBA3!eKhq-ZAa-kl{eJk>`kjoPbU^RU*u7rsH8KR|JX7j<#{7* zO;>i9?~Zflbd$$iRohzbR@P-^oiR*3kB)auSLDOANz6KbN*FSBndcVKmeUeiAs{EMpinQkjE6?~SAghP+Q%bh@SnZndv77UqmkqJB zfGX}6lD4AMo5Fo!3|_cr78zK18Rt$IqO9%$2|jEWs@SrZxsXHi{au26mgc?Kwq3PO zH9LK^1pgd54EE^NV7~bOE)&GXC>Fy5=r>_Y`s%bMMm~2K)=AkrS=XSV&&*}EDV;2u zsH9_}(H2?`>rryEfObsuq#heSEGR?qA?IcYu|hT9@>*CWx`SH62dSb}mO}n^0zKbD zbJn;X?{EHkE%{pwp*}@5UvbtS5T4`O)a_Up9yb-d`{td}Khc5ZundIqWJw96l0gz5 z9sX?1u9h>_4kU&*{zH{&=~&}4OB{g`)uJ^}Q}6{ndNtW_Ae9pSw9i$Vm@nPM=|mre z5_Ol`VhQ3u;st4MC`hEPSIxT6^%)R)09Av-af0EI*RWXO*O{wydZ@UVCooA!<3c+aBd+5l-m(y> z$!0$!RRQ*)BPGZfnr!nsvYHl~>~T}GR(YThUc4lAy=z*{Y0I3tDr@&F6(cpJDpBG{ z$hI5Ug8wJCx%lzY`}kGbaAHl1Bx3I0%qgX_J)*p0vM=+%9Z^enbr{FK;X?te%A=KxWwtCVyVXT7b(wXZ#=d%CKR%$F6m+^xD_R+HJ1Yy z3H7bY(+43n<_lG8L2afTZPkByBYmfIBRZ_q*;yKol{xj)s$=QZuMXfCWeVbgKPi}^ zqSi{%$Kwx1ylSQ|U{#~D*mr}mwGUkO0^u_o&xD#w?5S-q8ycG`A%$EC!V_1jY=ynCR zajwtBD&blm-|rTLhZOX3hd1tnk#hD=YpV54UTwE-uD1Kqf8{;z>M!cOp=Z5hU3kOD zii*T+J>3*7b&$*2?!lHwQNQBv@A_uFzEXJcb(@6#sqo+u+3C9qjeC)Sx9?Zz3cqjf z5Rl=xaB=WVS`P$4*KTusekc9A{>JV%cRCnf^H>m1z?&qe4WF8TtdyMt(?dq_7PJPX zHQs}a6sngVeyZ}J73i(lo4^VM02S1W?l_Jp8#kZVrkqco(Ln;Rjc zg8D6qYZqb4FYaN!UeL;*L)f%e)f$wlo=*tIHWGr>*v(zO3~;PC&2D};l-v#tvks=cpeC}afr8RvPbm@(T{h*1cEo2_`4XjC8`M^akOLw4Zs?LNwdHLQ+t%IiGjf)Md?~4 z&2{Kzr@+3HEqN$}U9DJ=#)>$Lk>G{a!N#RQ1zkWC@x`JJiTOHDzmu`{>zRxgqnPW6 zA37J~gyuVJRy)PkFqdhW6fiqXjM)wor9}V2z_d}^P0BdY;QyD}n7GQ~md09+7FkvL z$+!VW6*zFF+$Wo=MSC4V>W&);vCCDR#~i<*07J zD^pQ!x1bQzMJy+fLkhD$M3!}Hz7F5-Hoja6nU=5gJ$&uFK+K^k(LmF@)FIli;RK)4RFx9(v)c( zJ+^PaVI^mXAd}`5Sn>nn`%-xMOF3V_)Q;>G;NT9n5kCFuZ*-%ej<%9HioSE5bb1Hf zG3gKO5Qs*Q`LIoafCYG(v;xhPAc92cVV0sNP}+kHhqs0!OyOeN40jU0g8UluN1w*? zep9zS?PwBWiS7T}U!ZOx-%1Foyo5g9E{w`ZMgQN6@c$65&PBK`Sqhri?7s=^X)ykq zKQKT;nkGHBMKAqoGRuX^Rhuro?b^7*-So|Kq$hk}XVdzo_m=9KsKf}7(e9nz37NCI z-h>{caoXUIH0K@s?56Edpj_||HAlvcUeoF0(Heb_H7x|}7V;Di-eLIV zk^H$3;3vPXg!p&tl-hM`NTSQKFKwPIuJf2E{ZqXldam?D;saR<*ofHtIIAbT%?Y*Q zRDN1f5+0Y)o}woW-NmQ{bN-GH9>{`4agj?Kv8_=J^}ssJ0ny)~;U-729y3GBCB^#2g@4cVyu96I z-;)jQa1>lwn&e)3P|VmfX1~DUr*>iGe%*gd%~UEwuy_yzM=q2=nXPjT^X4vM5 zJ`^u}{RHwX^u29$}q{Iifh}kpi_9t-A;iWddZT<+>zh( zaISv;Mtu8;$*XkeRDtE2HeC(vk$^p6RpZqvmhfr^qdSo85We<~8mluawROcgWL6Ci zYiN7i$*FsBWbc9rZ{nt%x6EeRSehQcYW#5}bHH6-F|P4(?bhX{(j`|L%NHI|578AH zml4(tE^YwQ77hld8S^UiJ)1BdFP$3~Q4@MoO^;rub+RwSu*ITeyo-io&(23!L6Fvf zP|Vg<#e_2{*C4dFmXl&K&b&S;%K@Y7His6-A;chT4Yuei%!6w5V6o_M47f=`uAB(J z(Zf+bIPNX4^7YRkv;O6eGdf0pK0o=r1+|?`n_bklZ4K!kKX|e7rJ7N(?((v#Ni#|+ zWt{sPsShv2O^V?QQ+6HL$fVAVJ7@3=xU%J!yk2ZmPzR>OJA}6|P0pI_bL<85uN(D@ z*b&`YCiO7>GrQoJd9V=)y}&#!X^(#f(Jl#g4ZnfzapjT?VyDC?S?pQQ4o298aZW4U z73#IWpJ|#C`y5ESi-tMxb%({AXVcw{&~_gQvXF$3lgOD6Cdd%R|0Uu$)=gWh32}MT zPYpoY0JY7P=X(fFE&7%W-|_zqz$r~Sybkr62-GOWK~(ot-`f2+|2T&<_i%+emj}%IG~Hzv%TU^CV5Mw ziW+2{lXya2S6~JOmLOF$?CJ4?o~Y1-34b3B@rGQ?IVeaFSQBUUsZnG@MMVzD=%v6C(N9hL&d9!mDYmvd9HyH+AB>M=eDX; zGJL=}#Qyvc#Y$h*wSacY|1?+#0`}3Pq9lS)dw#^3zJZu}eFmg9FYGiI1?Gj603kqgUc4j{RZ zUvdy0qCQL2HdUK}J`4UD2*E_u{H@5p>^NtfrC5MWb}XzDh{t<`y*90@tJ;oHQa_$q zma}-{lwI5nb=whGJ->h8KV*936{o24z0(TxxmQ(kP7;-$abJ0$tNE4FuJ7-NG+cMk zxs1B2tK5y(3cFXk?!IqZMz8x;M7!8TW#&Ck7f#BoOly%I_&u0AJo$}oQJ!H+*W}qp zhckye1US*g2dAhll!>so7%(JbmkH zR+MR}Sw-tA`5#!0sK_{W&~AxzVfFUK1$NW{Kwx?n-99HarG^)nC;Er_X^waztTqpL z1T{tM91f==u7mH6gG@Z+MakGsEH?^Rv^Xy<7A(?5{Qw_+#_Tt&p>x!pF7yeAG2|xU z!eX8&Ic@0}SL_l`DGdX3mq)|Cr41VxHAGoOlH8 zp_NgA4nWR2>^S1-%%G#qJ)A^yUhM1XHs(DR_6UKV7P}3P@xG`SZ+C7V(e} zst{hhv>FZ!TN#ie@4v6vp>6=10F->B1d-9qt0U2ol)FBkA7ejC`TPg8F9fYOldVNY zM+WB*3p0%a zMnRRWUcsyrzwKJfa05*F^B?tv6E7uw1FPuI6ihkmy&`B$Qal@Nd)zm#jgL7LLHzBo z)}L|-3+lO;0fO<-7i$Maq1?M9qxke|QyC#!4E|sqUm|%mNrk*c7$6Ad=DE=?eKS5< zzPWD(0jSt`%cOfJX^%mU9*NY5_7!it-naf~`chyUq=f(@V*ISb{phX| zfVTXX56o8-WexL}`+RVN9jF+q73o`FwkLSNAkh!iOb8j^j7LfN|1yc)pQY-c;yPyZ zO{ej39yfcyQ$&KndnfN7FJkp$B>_FqXFFn>zsoRhg7BI|gKw9-WxNJEyt>biMIQ{9 z{tvaXP+An&7iWP-$DE@Ea}}MGsR&q|bl>|1p1=Fw&&`Sq|0c3(95-^RhNIvGIFr8c z?{Dmsrl^nMJPpDble!`1S>U&xJ(;aF??tvBv^N|uybl9YuEN&h?2;d8GwaHJa(2;A z@7@Ld)Np!TWo(yr4XoPWWMGs1VS``O>>F`Gn=deuixd?9S7V8(MRT2ds?e?RdJ+a{ zEy82sn? zSwQIG`o&1|Ro=x4Ts0R^daTa9WNm&T^m^qm;`bn8+=NeO)?+50myFNIS{J=SFGXv$ zG5VQ)-?%rYwRkc%kv4g*i4^|I-w?t?mw4}zt(9u)<_rF7(gR+?3ab^B`doZdmiYlC zo&L4aOM1G#{auv$>D+xEPJjQ~>a%KFukyt8S9J77&yS;Q_6vhy5bVS9R&5!rO-fQw zpB8Jm*ALCh*zsmlop77lPX4W`;==>GWo^YHIz+FsEwkh{w|}0~dGpX|{^W72mb!c0 z!%oY6HXb{%UQAOoNq-tS^BKS_o3q}#6sbxYzo+oWPZTQV)gZCcMGg(sAkbMG=Nw+? zBGrd=nw~u0`qQt5*iqlPyrTLWItBAE12iLTyv$4r0(Z(wT$&lZtC{p(nl9NWw{#cx z>EXiJZehM|&KeG`h5`o!)KngNKn)n97D zq1%?S1JW`pXForpo34$cW`BMpz4lJ`SEX@roR}j_+6Phovx{yxjGg6TNU4Vd@6ZcY zGR?)R+RM5-8Gdg}G@1K0D_w|vf=$#z8C5#Ns_v|N#yYmnb2V7eKeV3RFaN7f&N&_OUic5h)l^l`i z=H_bTsZF&kwUwA+?3w-KuA=X5^*?rTE9Dg7Q7#7Pk(@~QLR7oCY8T})cM^`P+F zHEeIND+}keU?>siHNUIoQ0*u={g(2^fj`J@Gu3AMi06oLD>aOVC`MuND^l$7wDZin zVi*k@U~d-Hc=5KLz4>W}1?)zEP_f>{M61xuWnPbP`lTC>v}ygmQQHeBEA}Hk5)3aw z$kpn+%Q?+A5XCbg8-ZIq7)ePq1^SfOW_WL@rc~}h%2aw4DN`5Lqr9SW2PeSL3+gIl z(nc<3xxi}oCTmL`*Y?HG{*N&c$13)bR`!}x57^h^e7mT$;48|o`h{MgRy^Z5yOw?I zW|n^UpR5DgF%!5DTes2gAmoCAm$`u%c+j~(aOw()3~>_^2q3B&>@2>1DJgr>@s@>A z^q@jJ`Q+rXk{8kdhx+9XQ0;B6g#^_WUBZ1sqyY8>4k2kHzAZeJs{0|9>(q1Sjp?VC zC9|xD&19~F{T|t29t^`uMe&f3n_TPJh4F&SAt>A~(C&aE>f4E{dF9p}W&b*NEF8B|M!lEq{ z(>-3v{PP4J1M3=y){YSRz9h-L+unWmm(71?nhpX=&XmyCnoz60y9jK=%|Y~oo>gp@ z$(uJLuUDZ`UwkSnFe7zl@WPys20Kgwp4@*}r)|{SQ&>h0g^V%$^{fcPkBs+yyDBTO zQ4imiAg(>ayT$-lM%D@V?Tq*#RXSQbq5gh@XSaNK&JJIq8)|My$k!X-jiOPZYB)7$ zRFEc4m)rvMlqDaz%q!PnBFhIL>|*H~eTAZGA?2orniNuq)x%KXVO5HN{-l1{J1Fd| zj+}q22vuv|;|^<&@9Di`ykq*@z#Tb>H>Z<}Cx&{MS*EfDufl089|jBKm>Xk2_jtmg zK6B~gjb`qIgSCW+#Hhbv+9cVGPz_&@Azcu_5`^t%EoM3YCh7s1WQVuhK4bNjllw{s zJ}JxGzxe2{A7MZ3ivD2mBXPH?+QOxwgQl3y&W|H&S&)^GHbm{?N!4~=YIizC=52Hr zbKlS+S-og0T zBS=r_SiL%&utODf_WmgXJ|!N`j7(R28F5(={FplemQO7qeq)sjfDv%jMXBA`Z)8}j zGE)EsT{sVy_mzdR-+p-bum(#T4C;g8HR^P=T}ebbdMO2_TFho~9@j2$3?FXl2gB>z z183y7Q}K#S;mkH&9RryFoIvOTSbNv5+vz7X+Zy0_F`V(y2A>eKh%=Ak6&F6-UguFD z9|yV{p=!BcpdgIDo74ghw6S|9iU-hlWzz!j)lzvbx=-_h{pF7x^aft#UWcO8TN3C> zN1d(;z&f??e&Z`x>KYfMuUwQl1+J-8i#6juX$sWqw!yzLp~-80%C$%FHw2a=J@zb1 z9(jMV%q1briSy}mdvN|jjfe|qJb+%XMwSxv6m^k(S6b$W%@5h~o6Yl} zb%rLzdD;~Jm=xhTJ;T!uY*rE&F%3evB(->C>;G5Ry9YE~x9{Wcjf05baVUZca*|OX z=nynTP=_!Wuno1y)I2g$63cHtkm8Y{V?y5oPq8XFts~n}O!yo%Dur z2<_a|{9?i8<%G>W@sKOLr7&<(z%3%FM6}%q(DJG=ay$<`M#D)&)fN8Ic-V0Ho?Kqt zE@oy%bT;k|dp>o*FPn68e}FVK^;smmqv5UP!)Xpr9$9tXs&Ga?sS_tIT4Vif=h+9d zQg-jXen8d59za{^I7h2J<>V6)R*ZC!F*?V0% zPi*8d`K8J@^}me2U&LUqea~F=-4JaZJ!NFrhn_s_guP*y9 zQ}kaZn-L4SPy>W%LDv%0Q}miCY>Pz4&(L1o!(*4Ilc;nqQB!n*O6dx8I~q+WrXm z!#ljpiMQ)o40xwa|xDyRW#gs$0Rd7oos)aGiRTMr~RO_*tS1c22#0sbyKAjKutEr}`3 zmx!i2>z+k5p)+KV_MPDVf|Z~Un3kxwszGg;o!fR~#GzhchLw8^oNZ>y1SN`*ctVab@U6mMHxDh+a%I&{=o|Ngw(qwM6QdyN_R4W`BD<&hu$IEg6^ z9;*K;1K;dZJ5;~^d3mqPc>`N^dh8=j4Lg@tyd9+83X;B8U%5i_@=`#g-{J99=ms8` z>P)ho%_NW;s{bJ+_7Edh`@OB6y{C=iJyib%+#v_8u)@O~Qp6l_b|?2gb{@DeQK61J zk1p4QC-M43uo^K;gIBC?&u>(XpHu}KQfvMgN%DREvF+NC(3`YR+L|%CSf1%0;OF16 z4c75TNnBEn8WB!Wjxxt8CaTv$$zd53PvA*;B}P|afDMxJ@?XMK56gA}cU->-w8+Yb zP(HE#WSe`fC_V9`^E$4=jE}t>%N*IBFtBC2s%X{R5ID}!+5h}xP!15z-`o~wP(j@&($VMY# z1-Fq}1ain}Lt6e(mn4FUXv2zkc;h#K`9|Pb3WH08BU{5AUi+=npG^WT3?L9K0SI?- z2bnhfU7qd954}p2-Brp0BwXEy zJz6Qe!K#orzeq0B4tUi_#iqN%JK_txk%6Dx7s{I~N+h&ul7`W&oX|>;j5SVT?efx! zD=^4hPmylNQE4Ptyeq(Az5bdNcE$pq^n;02{scdv)_$L-mObfv?Fbx4KdT24X-mYsw^{5|%E zG11@>3n&@TS(hS{e0BjhirsP6A9Q^!C1a6CzBZVjbMsris`~bS4K{sC?uZ=&Kl?KH ztrdP=oh^z{nETA4ZwM{I$==e}nzCGTP4;KMxe=Ahj%RHLVDq~EjsA&x34v~$K7cBv z{hoavKi{bE8SblD#WMhof3r80wX9tJQCWj_OvRbRUG%(;k3kwV^cN=x*-WWS_tYBZ=QM4HaAe7!*a}@ zc9_sywsKXli}dSxeuBlrletFBb8pD9itPpcdV)|O+<^}zsUT$cc52bF5!DMMGoAP80~fY=e3^$Ky-yJc@3yO(f)&FG% z&bS!Iq`nY;?hGD$JMuoW@XS8N%Re5(@BUhHb$NQ7&67Hb_*y7%#$NT}Yy7qL2;D>M zElR!w)=l)vE>658o!Z&K91)Hi=e@prBs{xz^ezfYxf%RRPA9Jp7;j`|KHFZr?m$q? z;pT7KKRtUQ$?(oz0cV9JP9(mn>t#L3&)W@S6^!u%=Z6S z&PbfG2-2?exeLDw6vdJHy^rmS_N{!ToX4_zaL`$z28qM11S(QFMCDmycjqA>B(Db| zk^ub=t^&tvmva1s-|1C)<=vwRoc^bB%w@%s__s4-Z8Ug1xCL)&lf8+HCVtUT_;SX` z*4VknlR3@1NjJM(I8G&nToi?2=p@GJ}HSiYixv|PjdQ!Oc->FcgxFg*Ja=?Iadpp6Qq1G$-Q=ukuucDWL zji#J@Bz9eONA7kd(FPgw%$iyERT_GBX`hYS!vfy^!f~UvJFY zD6c_hfy3{{@wWW;wH~aJIB_L>M}K5D!Re-knRJ8(sHgXn7bsUKnH4!Qs6A`!vG+kX zJS&UCBnJWK$m)IVxByj4t9a`P1_9W=;{kHke`}2%BAVGssbu7Rx^}$lW4XL?L?XP> zNCGSjE9^P&tQ!ZB2c%z*)~RjU?T<)rB-vA%QDc*-!^MM8>MQgBp$XQTeGl?JB#p-* zM$e$8&Sw7dYv%gIQ6uEFF^GvU7Ln3fao$P(L{?ao1d``ECx_F)yl=*ga~xai^-Rqe z4yZ}}|%-}Ica$R@hXk%Ptbi+YUwA2cO|r0xG{!+)7krI(J+kWMzK5^OZf18rQZ z(rQt`v3DlbmwKe=hto|@9!MTy@<=Zz@QhRU6*AFkzlkp6HN7r{zzi03}yna38? zphKtG3Rl6stD}O4 z-F(jb4#3B^Y!FQsnbxle$jy;gYRTc4w)cTw(idURV`71a(bcW!@;~HHZsPP;%S0(2 z;py{YVxlTN$qxd64niX_IqN$2G7BqfWXb+lDKqqUA7BbL+KdT(6+$)zMF7VvVGJfm zw9DJhhdpGxCZQ+X4UOSMgtHi1Po4_D3csA>4m~z_`Y)RlK0E|Q786Fc7PbX`oqnU8 z&*admSz2Mpbz3cIKxU5-IZE_JB6-zsott)x3m>^KxdV(b5@Ai1+=129q!0}3NVjE~ zvf-6DA$L)jxkiP7&V6x*32&0^k!HWZTVwJ8CVjVOk57jlzYN0{C&0Cb^T@?9pnOPw zqC%^y<387k)+qQ79whv{;*`P*Ian-i`8@iKnc;6uub{Fpm8ma(Kjv|TCD%@i=A}+TDi$qy z{P#;aY2Q?C&j96-KOm*N*|l-J*|!6a+nq)q^Z)QOSA_JP@GRab&Rp<(g(b|vo|2T3D;pt{{H8AadcW9pXm2!>lz;qb zOSS#QlaUW*l`T83?zY|J!gX8G&DtqShpXEqvOPO^pM6Ty=ip~4iwY=27oiB}F>S`^ z!wU}_n$sM3VXufOOEDe{-GAoG#-}wh?sbxUNIA!njfg>q8tRo$X#Yi0O9Ilu=ps_N z;8_3kaD2u0p+l<{&EBVfcs5kP8J`|fk4gx0?A~?$xO!k#w<2%F8eQcS>iG*ukylxH z3xTFh!(<^Y9}yvW*<%+~qJ31b`+T&c%B);qSs@Bm4~$!{!<0HO5&6e9R|jB!c)Q9y zFDvY;gU*QGJ-7e+Q5dIo`7B?pKG67I=0MdwXDAZ9k?RtJ0C;g^p}xa>!Bm#FaC*af)xpFIXb4Y(SD%1BUh{-*%Qtvg~r29ciC*qwNw%0BqqGS_yXs#%(oc=d@1JJ8i3C9l8IO+ zO3n&#&hmm+jjPAZnW=Cc47(h#v7#cd>-+xY*t4fUc8pC=t2sQ2N-vWlh1)x7C9 z!l5A!Afo*0-TZ~2Nz?AZ4k3W(v2s~hrU75IX=h&KHIJ19`u)!1_=f5~s7Xx;UpXVd zb<8}Md9$T<)>8A}7k#o!m9(%BP7^$;#z#mD%MhTg;TL2y&J(sU>#NL<5`;^B-tckt z?C&NYVQ~5l7S;$RMM4k-2v5`;0Q4U|i2k9ZFt&F@^S0BMi^s-bhFn_o@xOV?(9q0; ztaM2jdwDnPIchF=i&*I3g+T|8Bh&hOAoj6rBx^AXMwl^%J8y;IZfg%X111y91V_ZT zaj027p^kZ&Yr{xw{y-6snbY=Mw-#{$ahOi}a-OVzSN2IrHWVr|h3d|X7qkqM%|{2{ z6w0#?SvE2PQ`VgO&)@#`8k83uu4Kr^g;YmF?DST)TQC^;+%Xx+voKNYg?L;bCKS%@ zkhIADqSiMY3N(+S5~NEty*BZVK#RsuXI<*z_x6D6Ii#K(*}FWA$5c=IY%S09`##LA z@aNSuZ#zXtu2otY*;9d}1e~C#WDYp1l@K-ovxw8iUNg%#;OLOx)s^6SZvP+P9?lPl z;3DSW$QLzL3-pU_J(CgE6UcnztaRaL+iZFn?F+ko$aYTsjqwaQ=N5N_w)4JZBA_JroFL+kLqlf zmPVx*3krVfm?4iCxZv`Ceh={UQM|oG@WyhWu!qwD>|<(!(=aQCGZm?-bDz9I5r=B3 z;~Ej(E7C^;I~P^XGlkZM5e@X|ZeA@F#xZ`VG$1vKiL!mjb0Wb5V%FWlKv$)>@-iCd z5&fE+e6;A5a&A8G!3e(=za1B$5aWON9ZuC#dC~0w_B(kFsp6pmWPb|Omd%IDmtiQ; z2bfk`I8S5s%W&UkHm)|oc`nRawWF(X?1f638jbaQOxm-`F)uqU+jvLD6`v~fyj(&I z`k%_#4R(TJs6IxfkN_Ef3a4j9XN3=JQEpX-rC3LcH>gS*-VfE-KfA-E^#48B|H7x` zjRy{1cYdz3t(bfNa)n-%SGN0bbKcqd*lWo*&@fZB>xXS^b{V`T^D#Xv>@{D9Ny*HH z9mAG*HB@}4+%)QsC+KA`)R>RJ|3^6nBzJEVSy(Mk)z`pbCnvNf{5*NxA!1Yhy`vP& z^BW)kGlJgu3$+YDoUrm-i*YR%s8nwpI$i=0RF~x`Q%&V@hMEE;UrtROxtC;2AfZRc zTB8esnR3S}_Pq_MhO3)i?049GT5X-2l`r&Y;~6&5<{<Tt{m6LNzJA-^ zvk4lq?{2T^Xvvux|H;cMg4z;4sc9vW8ER1~hPQ~@n2x0FX%(QZCW*GEmBV*0F3)>1 zU2wi{FF{S^=<9AtM_f5B%~j{mAINvN^BoY-5kJQm-%r# z(|R%wj3lANf94-eh~sB}+XymU8_#B+Z~}%{B$p}rwmpfZ78c3hr}a?9+R|km2)fIK z@4}?QaT8Yd<>w>4rJ5Wt##pEBN-4ysCX1quVv3+T)TX1)DaJbPg&WEY~RIP5+GgA;IHijqhGgvx zXc(fmFZ|Vae6L=iRkv4PRnGSq{8%BsxBZw?>KMD54W|?E>rfRa?p& zznaXsF*e;?-&a1x#>u&GV4g3x*5Cc!n^~siCPULe{EmnE4tmjNc)mC0{puv4{jqR; z^Sg1*89I1wJX5y(vjT))RB&Bs{8Uw-6p;{54u_Z|Tv>WRZ z%gRtL(GBM-sJ%dY9BHEiazmmnh{{V7Mg}1bZn!`GXo3y@zEkI^FDDGP{@dw;4gYjB zSj`Ml^=jP76TbdZR^(amhVRUyGvq_F$F8ZzuLLg8^l6NGSb zRQmDwik11wd47HQ&$-dL@0d$kiJ;7C$gWAmO_5Is$$`k}bchyrgaE744K}(y3-33{ zan`oiEBvN_4sSh9o|(J~LsXl0rd1%$zheUMe-E4_`~iIMb9KDBc%X}DvFqhRM-;CY z=apK;o~-!vp@1Wm?G*gf645I9!)Ht85{>Y#C+|C{H<7f$9WX4k6;7jJmJ5C#ef zYPq0Rx2+m)tX%Bq3?ZftQL5v}a~ckYG6jF3<-t8H7W>3kyL+`ps}~b_L_GAaa?*mn zn6{$7BUoH=v@io`l5)EHZGm=wuT**FhkoHHyz!EF2UyavAax(B0;jv=UK2_W_`ii{ zJ}DRcq6YSa&XMx*<+ci^bNk-yduY%SLECJMiV>gM)?Aoe(MX5RY(v+IRff;A7-C$n z0rfHiTPU}M)YE{dzNU=*#dj{X9?t2!6uNWe?L$F>J%=S8lOHxl`8t=_K#fMqIjp^k zQ|sefLuEk;^g77N;kMJHh~*KynyO2R_5-bTGt=|D-ID6(sO%7?Mt%53io>~L{>a== z5-RwDt$0vBMr%39Wgv%b?pX4q&g!g))NF`VCv_X#6|l)C9_Dp?*xB|M@#XTRpfPDr z@Q?VY`ucLlPa=cnK&n5%PkxA7N^*_0mG&Q}F9&@MVuu0u=gq?VSkvoEEU^l|mms6t zYIJ9jsGm!BA+^jNq?9$+F<`LIESP*u)Wr9AqsU_ky$Ev8gZ7Vjd#>t_R&-_~F{MG7 z1?#47Mk3nxwO&*2*7iFE*@K4+q?>T}m+`WNsKzWlNoSYU>I&tM%`tG?sL3}nkz&6? z4tzaE#lO0JA~aCYWI*V>2*C zn7HXlyAqsE^3Euh0A&94%k@rvc}_~fxq9gxmu>C0tKVIc=oXUx zk<-F!HjHrapNaSSrRR=_2*$7x@(w-jn%13{ZZjk>cjlLGuDqRaJ^-B;o~wAAHOl`u zeEz&Dp%5!tmT9r=U2dA#_{mKgTcgw*2$m1d#=koR5I`_y{_C&ImD*{W+pA+2wxQ6D zE(uF<)Dlb57&3eNW1CpNLBIScDyA~LHC+#fKf`fI0_`_#*bzK+a*DOb!AAHg; zE7Ol>-Q_K7Q8?N1>{t8n0eZgiq42eJ&{65sxX+HMmrmkmqPoIUSjx`8II+xZuv?^@ zHzm@iDrGFH|0>Xuc1N@2l9Ah}M}Z`&9mxzZbmR(PZy)~loWvc&cI*4sDN5COefbBZ z^NY`5dY>LLKvawhs-!sZm@|nBIe!~L=|8+FlT}aZ0NO?C(2^)Ca_DI_2Gt`SMKvTB zU+c|=9F9!QTuAMGU|iMH#U*!A#~&E*pBl(6*bwUFCcvg(u2wsvDu7~p#W14%54x{U zs9y16o4B#u4ag*DCZtb?SQ{dGMn59@XAf(vVYtl+){dtWp}4jeT{|^Nv6zY2B?nMK z%-b3^4YwbsBqaX2lq$C%kAg(ST%Mh92|PU^<7tgr+eoAaj|r$*Um zby+Aak&91Pgc!8MF%E^&D5G%3GZ379QhYmj*t{pRWbZ_#guWZtUX_f#75m6}C*-hR znWxu+9cwY5uq7VS??u!8jVqGY7n`yFv_5y990br3y_9`~T+1a8eBSV0HZ#06h7((9 zL!|kdRUqe1K)OsTXS~Y~;&dU`p|8l&MG=_BT+Umq9iGe>odq5I%sD8)Q@cq{SvTRs zbCqtY8DsLs_@Ow?Mm!*>l1)Jkhs3pjYzjyXDmGW)jmyA+wkaD<5=)L688MvE11eEv zUvRevXYJ-r(BJWj@7ZZMXetOYGGP{YnWn^)7eE4|4XP_0SPf5v4&=9jIn0H47h1)x zQU*kzR-h$;bhZ0Q+lrh*Q^}+WF3bkUVMG%&1ma-N$PAT`PRS2Dxgbf026M7QkYRu- z2XbrVO!T>W>+iUJt(2%gf)ZMmHh`a*>kF>)6|D{}RjCh24oakBK>E2)dTV&l|Jw!g z!MV-ZNm#EcS~xC`@()c9A1qHV%RHQomL-Q(q8+XihF3zwTF2~@9%b> zEcB4n!y#d7)lB886)!G2c`Ym6BDjzjUK?33U*Rv@cG$^(R^~TXK*M>{bCIt83&JG|PTqMTEfIdD`H z?|ur#Hv!7*VYAU>Z%x_0qS?!RIi`$L@{z4m$IUNuJIdbzb%7!XE#ZjvEQUAZKk}wa zFFB`{~STZypt-bZO7FWL6lHyu8dt7oE31vn$qiI}0ig9Yt!|=XaK0Q*!$J{Dd zGqN!xI3mvpX)D%@4fydo@45c!PCO;4#?YVd@i&0%N4}8f=g2BL4f(I?V|ybgi?T+? zoPt;>*b$l+;S277{KSe6DlXmE20e(K()jenfz@-GL^+p~woZed-VP3qJYQ7H1eKrt z#ULELxaw=;tli^Yo>IJBFFDnDc`sYIikv9*??)O6>)E4kv9f^kz!Z@Gp$$g$zE$q) zg}V_OU^CLI&gwi@J=auXXoqdHeR;OpRsN5Wudy9J7p9ziVUuAHXU@@5B2;Zy#Iczf+Vq{W@<%j4GcO55m zVrGz9yrtMg@g`j907_Ef)t*)tne{MYX11r1_Se# zEyNF7g^aay9zs<|^syu3s|C16Xs8X!SQi|foW;2*qSyrM1zGV;;<+c{;TAhWFv>oe zD7SWq%L*4U?<{|=bpSc?D$!{pj9#Mb#$PG<)QlYLs#fUohkA|J1A6dM^C#yXLZ~05 zxYfStL`M14PPYFLEg^#oa)3iGatKUu^Xi)>Yw(_TAh(UfTa@_Zx8URzoc%#huOGj3 ztHyF{yVr&=o}$Nk9bcs%yV>}D#22o?yuWr|RiQAkuwkD*q#y_^<;21yS?7&N`#iy5&>=rao z)Nz+anH0A3`tm|W4 zA6^vvkgQ!aVuw|mH`aSc##v=jmDSxR>hIr7=D_6|lW{X8Wz*c_zFZ+fW~k*+qxr8O z9P{Cj+7;|A+R|Ykoet3nQrUikL?a@R;_wL#6OF&3Ch*%Z*)~vWnEYHn(Kf&EhW%D_@LLWUqxkBCU$ zM!#@;n$#Q*CE==g{cl*v%iYBOSeqh8@ESvTsTOlkWFfdMSI^&yK)08_;OR z;=-t(+f4GyQO2w~u+3B+KDIV+i&H3ElYm1aw$E*ufAM50Z(7gS|>E^$>+iTB`$j9dk4m}x%^j0=d(#O z(0qShuz<9+TTMaaZ0RlX6(l5c4qVIlQ0cQt2%!uvKoj>(U`8kIoY7K;K!A<)&ivOf- zj1oQQQNS+*@yWAQ#74Iq1TULQ^K!##DfN5Pf9|E`QN?OHlMRMSV$ZMeGDT!;}1 zyHm&rSWWNE5yCzuw&&$vcZLuEB3r`;fFkZMbh{DEQkuzOpdm;xd3@R`7uCGW`Mls+ zU4sV(cr%4OHr-KD3h3vw`N(RVI#Ax0V6~p~x-mH|<*U!PRO-%!B{7?VPcN=)igtbh z7>hCeQ=So;iE!Df{)cUb5=c5+)ngz#namCXXo(mt3rz5tVFU2t#|%rC7#i$D7ATpj zjn$^o_m?w$Q&xOFLazM$yJYT9eZF2_3{{QpcxlQeyM1^e&xdJ?{MhxLuoNDWW?)&vIXl8UA%WuKes zZ`=Vw^j&FuXXSHa>{ouJw}XA^LE4)3xyzxUKez0k?I8YV##Y%;G~tuN@q7}pdh^z5 zjX|Iti3cp_FS;d9NDDuk6{Cj8TX(RE@o#LnuxXZ)sIT{dT9f|ZAQUMhSDbx-d3tNB z&|guu$bR#6-C3ukwSW3@eJB5RsYAQ8IOEdNgdoH(gt9|!9GuL)&rT?d${<&(>1_jI z37dsm@<$r1!M`O~%SHr2ZCbo@m<;n=$%souSA!iui%mnJY3#joqW!^Rj?#&-ebddo ztQ!Vw7<Ju!-9&4lKB6*8sVepHE-jVD-#wb zT8%D9mn>a#jIXoFN(egbQrxC@E3LD-x#F~X&;F~c4hWcABEG>w(jv?+VyJCwmA>47 zgHq)e8=h3bC`yreb*%}bzazfn?afFFJSqhbmiSa0@pRTdEW~uMA0;3>9W=`>D|&@W zQw&o-DODETyv28So|9f@E&ETS+U@XIiD>OOn|C9!H8z+Yk4V>Ida6w&!^d|>y_ty; zEDnhu*jJ&)MtBTfQ!_wdhxL{G=CV;fDl0(QccdTlYJX@u=F8k@jXqL!Rlep)xWh@z z<7lsJRx-#Scn0S3FzG0ftJV)eG>Jt?{jLwm+GOwVtCWU7JXY5F1>$0gRKYlM979fo zqKAh`>TR@cNK-R2Mi}mkl9<62PA<=@Y%qlzT{19!nRJC%^WzN}NdW+o^u;C4)(s^< zH*(xH>a5z8A@<|!A$(I+P4Kbf$rl; z_nu+$0jzw*+jh-cE~1yFK#@$XH+%D`FQBIWsAN==wwpFj!3ny+)Sjd87S$nzL(p^D zNJLFUzG&Eu!H0G^dyn^#st17cEW~1>Rj`)hv9?umUGvWB`F!d zQJvQkp}T_!4p9(vY%H8G7Wbe4PE@+o1PzB_8Deq5+4fqvWtuywD2hbvgyjo>d%|}4 z#da}hygg2a9MP`Vn7PN|^>;gu*KyB;N#b>tLCLlUCMz+t2LM|gA@!+Z$sFR(QXQd!UVNdNQ*??Lo(yHQ%^J>!^2yA$gx z3g-24dao4T$YcHohYx($`i;Az(E8oG+3x{_sTYn^pof5j6vdljOrN(-+wB{ta>!uK zCQKY*YIGfjskPEY9yeW%%5y_Uod|N+;({UE!Tft^QFeFpd~5bs3{Dllotb&Te#x$% zpTzc&1o$QW$Gb*5(?9$I4~nK1HY$#z)1c43#J~uj&Sr(zaK5~kM-k!L(lbDBG@EMj zIJ^_WN$E0g02k>vkv1Xod=+8NuQZ6i1(G`S$8NwaMc4(EULhpIJ2)5Akr0n0A7H zs$`T`f|Yk@xZi*N44%E}dXRFP|BDL=9`29EKb+~lbFH=M8^_|aqW!&d=Pn4Zwf|^U zWxu2a>Ttozc-|#m2L<{_T;w{sNk1SiMLr=ROF1tzQD$`+d1c!U54n+faoT8cu%hSSd02G*{=yaXxVo@G*lLRwF5$7dvCY(+~QkADQu= zO}|B+O`jLr&CaU3v}62G{dYSbO!GRxBLYI`F47_%wV|$q z1O!<^>$LCz2WOi%0!t~ifcvl+>;Cdvew#SZK7GyE zFeoxBG{bV*64whB={fJ3ylwL%leu#s+?dQej6!O~+sQ`f3XJ}0*-NAxPHOm-%|#05 zfe?WJJNdtiQ6-Zg}q^AfDAOq+Sfh;Ru;l$o0h*`W6MO{#tn>3V{JSF98*j(RZV zYYe&|=nq9o^mNI@KVnMVh1Ohw_ZS5#6 ze&OC#N~q@?3%AC1wgRq;*L+BP4C`93PhFn^JxS+|stNJrgI10YWe8H$S0nj#^K)Q+ zd{|*#O{Ovl!l)me7yMOLy!TVHp$5VMI=2-){8FB{a}tXkPVu zgL80)_M{b+o#&+rYKjWVn+Ojn>Y8$KN1v9=-ov9MRVOjRYm>viO^)Gzr6*pk>f<2e zd@iWr2dV`;DPahA#O0RXzGyoG>;_x^~nsIMn5d7udAG~ldYM4U}I@jTgx z(ky>8nNwK;1KQ#Cu9F#qa^IEhAT|2zS5<3qh*yKC)`WsBul5WY=DjL(cj}@w+&KxC zynkJHVA)~woSFPg!Q_?J^`oxyE?G+p!EhVy>!aovJ*^;sb>2AR%!{xc^UjDpeoCyf z$xaPYFHp;8K2Qg#!=ow|R0qg2gJQD6`8ELVTdit92m_bgzgEQ(`v*pmqPJ^{78N^kQM&#PR_k}ngL)L zh>bpuD8rPufK4FcmlFAa-L3rNrvAyCmogvmlP8x8)p5z3IHwezO=)4B%O`&Te$yKR z-?rZr@kONXvwkN8kNnllH@^C;kd-25Wq35EDwVH2#6(X|*`7u;WX_kWGo~JH$bu5{ z#^pqWWE2cRdc!k`{TfG*BNob*6Rl=o1|=9O>T1Khv##Ps!11=T1Br(&( z#RlE%pk$%WhijG`VeBUYEG(DvyMjXeNlN=DL6d8L5J~woO$z`IVu3T?yjI#ds^1<^ zd6w>g5UmUl|VZQH2NW6yoo$@&d8sh*GUnRafHYR=>5xwqS$}Sh&Qn(8#annPCW``!(%* z!JCF;A7noQN0Q<&h=?>NBbFgZzdaxArCVMaXrxK8FICsG&sP$K+C7HJ9?(CxpCu@< z!F`9YRN8f5%bq8nS70y`FonfQ+uB}A3<*}SK(uu--o{G_QFXMDP;} z$VMy^eKJzONF3ds9V7YvIuR1pmJ^jx!i&^WsFxaa{LNHT5%_&p9!~yKhsb0-Vg&GK z$gg0D=@;W<#Espo*!jq0Z2v!fx~$KWTsVb3LDDWX`6$2b-0OR&$kNaq3q^w!xKk37 zafVgi72N48D;W*#T{Z!O!L=0c3)t3lZ|q7Oda1+=vF5G$8I2iMT?U_pt1QU{gwU3L zz@Muf=NB#}eE2TYP3X-uaq#Y?ot!WB#(|(d-cXY}KNMnai7QrI^2lfmM@mW9H^M+X z8XxMUsGrY+PzC-LHFc*`EqkM``ExzDx6D>=s7F6xWnEB-&B!EZTDg8Dp`sQTVAZB# zAe4)Y!{KU7Rva>njr<9Rq9xnW`*}4ABbzY7Be{{S(l0~KXuFJdE)TMdz9PJ3W?Fi~ zsVA|Z$Z2^B+TVvJ_eR8Uf(@gcpj1~$01AqrHVgPm{{#+L&y>1J_yiCQB>%NMHl0}=$bWZ{n3uEMz-$4? z&2K|UV~Mb!+Ra)O5|?@YI;X;k46~2|^0&kU1G8}dRSC&ttBVw#8_c(23!%H+bDjxj zhDK~QXmmy!0dr|$!~M_>HNre907Pb{(r6H$-^xv2nY+W4@Pg43VXz!AZ^l+_)@BEO zFJS&!*HRp>4HzqeZu{tJU*7$xpcbH6$r|$uTrkUxMo@Y6LiDT;Ygn0yWvn53%SI*w z_Q;){L70%P%qS8S#8qesxpI;82(Wbw_M}ocB8#>Mm{^v&Eitmw8JCZigFyT-E>}lrP%lL6QpO`<=;T3Xzm)=Ph#@BUu1D$jSxy!InsC% z-)S~?mhJ8q)g}u>_60u#5aNX2G&g^tf7EUS`4VBb3Bo$T*8pagT&M~5pAN+6-3~xT z?ZU9kY#M5kT!1jkajl5eK#BA9L*NwO{AI{{YEX0)NxL?K{z(LA2$(Ir0WecS$N2gr zlj5YsQj;$z%r|_G->?=@0g+7t1$j7sr5HntK&WiwGe>A+qlj!VfJAgbs5paO-FjZN zQ#*Ck4JRUGOVXL16;ly+=pAUkbnlwoFyD0?H|WMrAJSB5V)1-_vW`nGL^wiY6{TeL z6MYf?ONz`?7Jr;biTvxg_3TCez0Y^}wn|nPXon+G#K$K~(EU>38>gLasoFpI?mg7* z-O*BRtszaAqtb17Y&n+Z7(^DvI(0+(x>nHpEesLLQ}1SoP`Dvilo9zVne)K38#=0Q zAcVM6=tTq73)b@dv<$vit0?O(X`mv{X|Pl=Etxil@P?}jR%vYFc6dkbGKNuT6-EB8 z1QYQ_g}~4N6(G-eCzn^Z*-jej8_DPLwj9C%-$U_Tf92&R_f~e9yM8}T6c@fnEX z`A)xH0W=ErA^c=lvO^B3_4*fEj1yPsJ{wNnU(|;a-Y3GNc8<3Vh;HcRjyd6aVX+26YK$9nfT&@Rdj~k3b(r#hyUtuA@ZnZg}Xm z+xPRAXjUPm;vO;`O8q1Yv;ORuc&m2&3*MCu3#ng493-Bw>vcZt5r6(&T+)V^0jHFx zeABlfRvi!|wU-5`LnBeEA99U{mg)tafaQ9wF}>25c(Hn<#?J0+(zMPOm52XOFI_N7 zz?o#(2|IZjQBFjV&t| zkh~D1-yo4&%O_B;$NcTXNRpWCrlNrUve|c}XO@$U%qD=+h3tsE7g_w3eeT?2(nJys zooyFRKpUSK4cufkC^|)~H+^LehyiY$8$sI7Vbn9T$!8@N#^RT}xr578m;)(G2zmll zUQc4z-bBH=UDvocRo>nrsO@;oU6VJEMtXGFR6Q84fA9|v{ewFatVUi8dmcP>2+iNg zaUoF_iLPuc3(aYj@L%1k5>E*Q?u0%wT03&@@g#V1R3W(h3CjHR#E;uIPr5H;TllE| zh36uXI<*dg_6vVpu@||10_+fZd_xBr651t7LYnt2Mb^kx$*AkWZ>`ebek3MQeV0>a z4;V-x_#7A^!EAwE6YNmG=bI1`!H$V2{z&~Ho(ViTT<@&cNiKWJ9*opLp6Gsr3Ga|+ zGA$7>3BKB%^QyuVPTTNGf;* z)LA|v{YuZPh|R-+AOS565oiuW!kDcG&@$n+0nbXa3WIE4%hxJd zy39H))Wk~oEB)V<{sU6`dG#D87o=bIaiBS7f3w(PJd8m9T!_)yvsatg@g1~z{_OjdCV z@|?`*(B?va>HI>}D{jX?6%patSeUK2n(}2_*B^GS4rmmYDICqms>antV&JsQEk8#2 zQQu_3T+!D+2iIYJS^4So`XbnV{nKQOCE@c+wgo%`%dbNB7#$j5dUk|lg>O3FDjhA_ zc0_P@BEckEFb3VyDAcpeiBhw!OjT#qpq-%3QnCtpWKr1{>4n9CB;njCQ?#jQ^06q3 z4!^}~#vntb-}+l?99lm)!$u)6G+zIApxYH=F=o%l=1T*S+X-q^?06Z%Ebp!du@PU* z7q9<;{uZQ>&E=#_KrS8@A6sU*?|i=Eh)%R2X*G&V;eFiT&?nfh#y5hUgKzHHbMjM> zox1Yv)lp4+NF0#%Zu~0P8^~ucz%L*!!6w*)jDa3=e*ha~?ih+S_yh_s`^VG*`_A=7XPH^!u%S*rgdR5Bk7s-gS zed&+j$8aN%)v%iakLrz1tMrPb4?EB7d6NV4<~k|29W~P$UcaQ(Gjdi_b&S zV2!Tj7o)_?IHv-wVGg4NeX*Nj$@6{#sZaI1f0OI2Dl`ZmU#QMA4MMCznr zq9*xXa~v5ly~vlDADEnzBl$j8?gZVh!nG#??r;B4vkA1Pn1q+7&@C<&PCbzw(1EJ$ zt`_NIqR(JuKZbNudc9|SYXEAph^kb?0~78mA$8L%15auN1(PisZkEM58GF~D)A7wN z5B2~8Cw*m-kL)Z1n=&6e4BZNQNLY4?R8G8JBYjB zccLNnl-11MjTM&?U)=EzA5?QJAyJh@H0rz49!W^YN#X^KOza61wv&bN!NXM{IsTKM1x7pQ;ZMFNQ>$?Csz15R;7o=VK5iQJd}R4y54| z`F*j8D?p1ea6EGPPy+Y2vd0^6d=ezg0NkKcG(=)5_e;jfkY zJt4D=%*V1ZdK}W1yoIuHo#^)`O=^cB0*8LyC4$mAY|^hVkT!*n>fK^x&UfbTh~p!r zN4|T8P_!_r0t+7^`LU6R0A#otVLH)*b_!j@;BnlMy^HE#R7axf+&`qXVdxvDPm~md z&7Y6HcS+?9qT{ByQ)j)gh_vSzuzmD2kgEt!i%yX;g$KcehWmNG{)YDC6Il*)&01 zY~F{tNsmz=j?>Y{r{;|HvPG<2T(4j!pLdtKAphG$Q>Y!*^mm)(QC6>x@jUUJuW z93q^%+U}>OnFP7-9N?r?exB|3xHIUi6BIZsIUZRa9tkNM+_diqP!nSE8(Q<*DuGde zhlL#T92)D%PSWxSuePnEmI=#i2yq!3ZvSI?MeP(@@jJ_y$;&bB^3Qe@YHT~ie;f*^ zk25l3eXKELJH0`#vE{L{u^tst z?#fJQ(5Z$6n6ABcz~yZy<=WR-Ic!wiH2uk2#$!I#1XlbWmGHNwH^)}F9ruyce7Hha zXn@R46RAuq_z5I8gj}rJ7A(vpYZ8LdgD=8IaKf3w2ob~Of8adxVE*mWqprB$p#UXxdGX!z=N<9!SkWYdg#a(ujid+qH1S{E%gbGD2M3 zj$tSGtz-pbrHO}$?-iiGZ6PW7u zkRPzpmnm(#JvY5DSEZ;+>Bg3Llv` zXl=s=mI&!SVzMaN#bxDGcDYe}$QyObps1hI&8ltfcIX@M#Eck^&W(Ff(;$698rzi- zKA^@-FO0k`#k2=z@$JZtHNIEFd;hTQwfPvPiE@r9Q&v|F5YG_<_5%`#Y6z7r@__8c zOXb+ule}U8flipubDf-kXOUtitY8-z$>Z|b<=0Ng;68JjyoTJdwe>%+rqg09%{7qC zgi1exsl#aBA6&lS%wF{Zu-JanPAIOJU+PSRh0Z-E^>FDz=^Y%3p`=_%kH=69jP!KK z5{p)j2-Rr=y`bE?7`f)+oI3JdyU-!$dk*@XTxIO`q(1$U!jTcTmM^bZ+^lfPQWlj< zdnK)*YV$v0_7G{H)%yeL)wjEZxDY0PTAQ|sKmmm@h2i!$)fhsnWWoRDPUf9)~iz;oi)7{rsIq8=vNFy&UJZXzguD`wV+{Z zsE$myKs{JDkN)VSC(9M-6QyWbg(5H5*QS)J5nNyucI64bB!*MxbV&OdSB3_R+ zhHW#I41H(Y#5Ub@GiAT_fCJv7zmpWVIMqZiSJdQ7!TlZ&K)e8XebiPKWb8*o+Bn{% zjzJw1d7t_CjDXv5mCrKv(}wcCSw%H-IdetwnHR0i29?A1z7t%;zfLe4`a5zxuU%Ii zdnLb8;pfYlcrxy~p@a9kUa-ez&W^neBZqOYI5(`EsG$f=LaKTi%oB!>r#ucmJZHe&g zPi4p}zyczCE*dLk+6YHqpNo_*=7K%51FXW)qApQcc29T+GHu}5?Ua(80LS$UCm(kT zL=h{Q7*@-^b^Yn&a6%|fIDbLO}f-KNV>_n#9is&^%(tu+P@v zsL%%@Q89sQNeo4nqeKp=?P8>T!()^=0K5F%2XTq+t7D^YBg$oJ#DK(u@rA`4HOuh>x_oj?ONo^U%*d_mpf>om1MxaEh~>R`%m`Ke<-9{&#gNs*<>82nxVwp2!|Qq* zW>H&A7LMjHu%>4h){5vlaKD07ZjXi!F*M{NS=BWRdhC)hCZLTINbu5uO2YS0SpWm_ zOE0%Z?GDwZM4f;ypf``iUS0Pd)fWy_{{Xh(zj+Yw9#~JFg>0k+n4nhK)%}IoYk_hW zoui5>6(X5amd%b}mj=q_AR>3j_CAs=xQT?D2A)^OiuvTc{vQ4HW)V)%))tKO1A%xx zrpG9){x}r6J!MLnf_qIel=t~O>1c=m3DP9XJWn`#D7>}$h^4D4G0U9j0VUrEKhF;m z--t-J27dJ<=I~kpbEu!s+?1UjU^Uss2)Q(D&Ln$zil9PtRFdvT;6EJ+BS*GM6Vp~z zk>r$x7m>q|FR)0Yd3MWPq_6k*-?UX$UgJ5uDd9@dZxy!OiB}p}l`=kEX^k2d@>rfj zH$Q<*X0TqU{H7;(47q#uvTn|fKChn0Msv{#LKgdfUI`aY+pG5#?vy+x1FDizpruD| zjb%ZtwWMZ)o$@HrM(tgmxYC^e>SPop!jtVJukM!L*u0lIjM->+WZs_bAGT{qlbfY% z)n&0r0*MCzr5^gO!pnPJ@$(25-QuapA)?KM9VK=(4V0iQG}$p$Td; za{$>VrrLwzwK(q(6WS0XTaUSr9G{K+8dTA)W{M2ercAbj1wSrAC|04dejF2h-SR+j XZG(&+b=If=7?C-Xkma-g{{H^}jh~)o diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png deleted file mode 100644 index 7b188191a193758c725eb2b86bba27b717611517..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28411 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�m5Qz)D(e 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 iq{{yf|0wET*i|~lBJ PolygonFactory.CreatePointArray( - (10, 0), - (20, 0), - (20, 30), - (10, 30), - (10, 20), - (0, 20), - (0, 10), - (10, 10), - (10, 0)); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnsureOrientation_Positive(bool isPositive) - { - PointF[] expected = CreateTestPoints(); - PointF[] polygon = expected.CloneArray(); - - if (!isPositive) - { - polygon.AsSpan().Reverse(); - } - - TopologyUtilities.EnsureOrientation(polygon, 1); - - Assert.Equal(expected, polygon); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnsureOrientation_Negative(bool isNegative) - { - PointF[] expected = CreateTestPoints(); - expected.AsSpan().Reverse(); - - PointF[] polygon = expected.CloneArray(); - - if (!isNegative) - { - polygon.AsSpan().Reverse(); - } - - TopologyUtilities.EnsureOrientation(polygon, -1); - - Assert.Equal(expected, polygon); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs rename to tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs index 3606891ae..74710b3aa 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs @@ -4,7 +4,7 @@ using System.Numerics; using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; [Serializable] public class TestPoint : IXunitSerializable diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs rename to tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs index a5dcb4880..3a2882ecd 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs @@ -3,7 +3,7 @@ using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; [Serializable] public class TestSize : IXunitSerializable diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs b/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs deleted file mode 100644 index 485413515..000000000 --- a/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; - -public class IntersectTests -{ - public static TheoryData<(float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y)?> LineSegmentToLineSegment_Data = - new() - { - { (0, 0), (2, 3), (1, 3), (1, 0), (1, 1.5f) }, - { (3, 1), (3, 3), (3, 2), (4, 2), (3, 2) }, - { (1, -3), (3, -1), (3, -4), (2, -2), (2, -2) }, - { (0, 0), (2, 1), (2, 1.0001f), (5, 2), (2, 1) }, // Robust to inaccuracies - { (0, 0), (2, 3), (1, 3), (1, 2), null }, - { (-3, 3), (-1, 3), (-3, 2), (-1, 2), null }, - { (-4, 3), (-4, 1), (-5, 3), (-5, 1), null }, - { (0, 0), (4, 1), (4, 1), (8, 2), null }, // Collinear intersections are ignored - { (0, 0), (4, 1), (4, 1.0001f), (8, 2), null }, // Collinear intersections are ignored - }; - - [Theory] - [MemberData(nameof(LineSegmentToLineSegment_Data))] - public void LineSegmentToLineSegmentNoCollinear( - (float X, float Y) a0, - (float X, float Y) a1, - (float X, float Y) b0, - (float X, float Y) b1, - (float X, float Y)? expected) - { - Vector2 ip = default; - - bool result = Intersect.LineSegmentToLineSegmentIgnoreCollinear(P(a0), P(a1), P(b0), P(b1), ref ip); - Assert.Equal(result, expected.HasValue); - if (expected.HasValue) - { - Assert.Equal(P(expected.Value), ip, new ApproximateFloatComparer(1e-3f)); - } - - static Vector2 P((float X, float Y) p) => new(p.X, p.Y); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs b/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs deleted file mode 100644 index 206985699..000000000 --- a/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Utilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; - -public class NumericUtilitiesTests -{ - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(3)] - [InlineData(7)] - [InlineData(8)] - [InlineData(13)] - [InlineData(130)] - public void AddToAllElements(int length) - { - float[] values = Enumerable.Range(0, length).Select(v => (float)v).ToArray(); - - const float val = 13.4321f; - float[] expected = values.Select(x => x + val).ToArray(); - values.AsSpan().AddToAllElements(val); - - Assert.Equal(expected, values); - } -} diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index 494e80369..455b7fe84 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -6,10 +6,11 @@ lcov [SixLabors.*]* - - - ^SixLabors.ImageSharp.Drawing.WebGPU\..* - + + [SixLabors.ImageSharp.Drawing.WebGPU*]* true From d06676f35b49747dfe0c8372f24c70738065a7f3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 11:40:24 +1000 Subject: [PATCH 083/136] Remove unused type --- .../Processing/Backends/CompositionScene.cs | 4 +- .../Processing/Backends/CoverageCompositor.cs | 71 ------------------- 2 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs index 2393ce20a..0375d8ab5 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs @@ -9,9 +9,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed class CompositionScene { public CompositionScene(IReadOnlyList commands) - { - this.Commands = commands; - } + => this.Commands = commands; ///

/// Gets normalized composition commands in submission order. diff --git a/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs b/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs deleted file mode 100644 index f8ee17f36..000000000 --- a/src/ImageSharp.Drawing/Processing/Backends/CoverageCompositor.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Shared CPU compositing helpers for prepared coverage maps. -/// -internal static class CoverageCompositor -{ - public static bool TryGetCompositeRegions( - Buffer2DRegion target, - Buffer2D sourceBuffer, - Point sourceOffset, - out Buffer2DRegion destinationRegion, - out Buffer2DRegion sourceRegion) - where TPixel : unmanaged, IPixel - where TCoverage : unmanaged - { - destinationRegion = default; - sourceRegion = default; - - if (target.Width <= 0 || target.Height <= 0) - { - return false; - } - - if ((uint)sourceOffset.X >= (uint)sourceBuffer.Width || (uint)sourceOffset.Y >= (uint)sourceBuffer.Height) - { - return false; - } - - int compositeWidth = Math.Min(target.Width, sourceBuffer.Width - sourceOffset.X); - int compositeHeight = Math.Min(target.Height, sourceBuffer.Height - sourceOffset.Y); - if (compositeWidth <= 0 || compositeHeight <= 0) - { - return false; - } - - sourceRegion = new Buffer2DRegion( - sourceBuffer, - new Rectangle(sourceOffset.X, sourceOffset.Y, compositeWidth, compositeHeight)); - destinationRegion = target.GetSubRegion(0, 0, compositeWidth, compositeHeight); - return true; - } - - public static void CompositeFloatCoverage( - Configuration configuration, - Buffer2DRegion destinationRegion, - Buffer2DRegion sourceRegion, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) - where TPixel : unmanaged, IPixel - { - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - graphicsOptions, - destinationRegion, - brushBounds); - - int absoluteX = destinationRegion.Rectangle.X; - int absoluteY = destinationRegion.Rectangle.Y; - for (int row = 0; row < sourceRegion.Height; row++) - { - applicator.Apply(sourceRegion.DangerousGetRowSpan(row), absoluteX, absoluteY + row); - } - } -} From d8a57f8e96ccd1983e4572a2f5d599e948d9b149 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 18:28:54 +1000 Subject: [PATCH 084/136] Reuse WorkerScratch across rasterizer calls + optimizations --- .../Backends/DefaultDrawingBackend.cs | 30 +- .../Processing/Backends/DefaultRasterizer.cs | 470 +++++++++++------- 2 files changed, 330 insertions(+), 170 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 1d7dc6366..ef84288ed 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -71,9 +71,20 @@ public void FlushCompositions( compositionScene.Commands, target.Bounds); - for (int i = 0; i < preparedBatches.Count; i++) + // A single reusable scratch is maintained across the batch loop so sequential-path + // commands (single-tile or multi-band) avoid repeated pool allocation round-trips. + // The parallel multi-tile path creates its own per-worker scratch and ignores this one. + DefaultRasterizer.WorkerScratch? reusableScratch = null; + try + { + for (int i = 0; i < preparedBatches.Count; i++) + { + this.FlushPreparedBatch(configuration, target, preparedBatches[i], ref reusableScratch); + } + } + finally { - this.FlushPreparedBatch(configuration, target, preparedBatches[i]); + reusableScratch?.Dispose(); } } @@ -136,6 +147,18 @@ internal void FlushPreparedBatch( ICanvasFrame target, CompositionBatch compositionBatch) where TPixel : unmanaged, IPixel + { + DefaultRasterizer.WorkerScratch? noScratch = null; + this.FlushPreparedBatch(configuration, target, compositionBatch, ref noScratch); + noScratch?.Dispose(); + } + + private void FlushPreparedBatch( + Configuration configuration, + ICanvasFrame target, + CompositionBatch compositionBatch, + ref DefaultRasterizer.WorkerScratch? reusableScratch) + where TPixel : unmanaged, IPixel { if (compositionBatch.Commands.Count == 0) { @@ -177,7 +200,8 @@ internal void FlushPreparedBatch( definition.Path, definition.RasterizerOptions, configuration.MemoryAllocator, - operation.InvokeCoverageRow); + operation.InvokeCoverageRow, + ref reusableScratch); } finally { diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 2ff6a8170..be5d9ae03 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -33,6 +33,10 @@ internal static class DefaultRasterizer // Higher counts increased scheduling overhead for medium geometry workloads. private const int MaxParallelWorkerCount = 12; + // Minimum number of edge indices in a bucket before sorting for cache locality. + // Below this threshold the sort overhead exceeds the benefit of sequential edge access. + private const int EdgeIndexSortThreshold = 32; + private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private static readonly int WordBitCount = nint.Size * 8; @@ -54,7 +58,39 @@ public static void RasterizeRows( in RasterizerOptions options, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler) - => RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: true); + { + WorkerScratch? scratch = null; + try + { + RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: true, ref scratch); + } + finally + { + scratch?.Dispose(); + } + } + + /// + /// Rasterizes the path into trimmed coverage rows using the default execution policy, + /// optionally reusing caller-managed scratch buffers across multiple invocations. + /// + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + /// + /// Optional caller-managed scratch. If compatible, the existing buffers are reused; otherwise + /// they are replaced. On return, holds the scratch used by + /// the sequential path (or remains when the parallel multi-tile path ran). + /// The caller is responsible for disposing the scratch after the last call. + /// + internal static void RasterizeRows( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch) + => RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: true, ref reusableScratch); /// /// Rasterizes the path into trimmed coverage rows using forced sequential execution. @@ -68,7 +104,17 @@ public static void RasterizeRowsSequential( in RasterizerOptions options, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler) - => RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: false); + { + WorkerScratch? scratch = null; + try + { + RasterizeCoreRows(path, options, allocator, rowHandler, allowParallel: false, ref scratch); + } + finally + { + scratch?.Dispose(); + } + } /// /// Shared entry point for trimmed-row rasterization. @@ -80,12 +126,17 @@ public static void RasterizeRowsSequential( /// /// If , the scanner may use parallel tiled execution when profitable. /// + /// + /// Caller-managed scratch for the sequential path. Updated in place when a new scratch is + /// created or when an existing scratch is incompatible and replaced. + /// private static void RasterizeCoreRows( IPath path, in RasterizerOptions options, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler, - bool allowParallel) + bool allowParallel, + ref WorkerScratch? reusableScratch) { Rectangle interest = options.Interest; int width = interest.Width; @@ -136,7 +187,8 @@ private static void RasterizeCoreRows( options.IntersectionRule, options.RasterizationMode, allocator, - rowHandler)) + rowHandler, + ref reusableScratch)) { return; } @@ -152,7 +204,8 @@ private static void RasterizeCoreRows( options.IntersectionRule, options.RasterizationMode, allocator, - rowHandler); + rowHandler, + ref reusableScratch); } /// @@ -169,6 +222,10 @@ private static void RasterizeCoreRows( /// Coverage mode (AA or aliased). /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. + /// + /// Caller-managed scratch. Reused when compatible; replaced and updated in place otherwise. + /// The caller owns the lifetime and must dispose after the last use. + /// private static void RasterizeSequentialBands( ReadOnlySpan edges, int width, @@ -180,7 +237,8 @@ private static void RasterizeSequentialBands( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, MemoryAllocator allocator, - RasterizerCoverageRowHandler rowHandler) + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch) { int bandHeight = maxBandRows; int bandCount = (height + bandHeight - 1) / bandHeight; @@ -189,74 +247,56 @@ private static void RasterizeSequentialBands( return; } - using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); - Span bandCounts = bandCountsOwner.Memory.Span; - long totalBandEdgeReferences = 0; - for (int i = 0; i < edges.Length; i++) + if (!TryBuildEdgeBuckets( + edges, + bandCount, + bandHeight, + allocator, + out IMemoryOwner bandOffsetsOwner, + out IMemoryOwner bandEdgeReferencesOwner)) { - // Each edge can overlap multiple bands. We first count references so we can build - // a compact contiguous index list (CSR-style) without per-band allocations. - int startBand = edges[i].MinRow / bandHeight; - int endBand = edges[i].MaxRow / bandHeight; - totalBandEdgeReferences += (endBand - startBand) + 1; - if (totalBandEdgeReferences > int.MaxValue) - { - ThrowInterestBoundsTooLarge(); - } - - for (int b = startBand; b <= endBand; b++) - { - bandCounts[b]++; - } + ThrowInterestBoundsTooLarge(); } - int totalReferences = (int)totalBandEdgeReferences; - using IMemoryOwner bandOffsetsOwner = allocator.Allocate(bandCount + 1); - Span bandOffsets = bandOffsetsOwner.Memory.Span; - int offset = 0; - for (int b = 0; b < bandCount; b++) + using (bandOffsetsOwner) + using (bandEdgeReferencesOwner) { - // Prefix sum: bandOffsets[b] is the start index of band b inside bandEdgeReferences. - bandOffsets[b] = offset; - offset += bandCounts[b]; - } - - bandOffsets[bandCount] = offset; - using IMemoryOwner bandWriteCursorOwner = allocator.Allocate(bandCount); - Span bandWriteCursor = bandWriteCursorOwner.Memory.Span; - bandOffsets[..bandCount].CopyTo(bandWriteCursor); + Span bandOffsets = bandOffsetsOwner.Memory.Span; + Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; - using IMemoryOwner bandEdgeReferencesOwner = allocator.Allocate(totalReferences); - Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; - for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) - { - // Scatter each edge index to all bands touched by its row range. - int startBand = edges[edgeIndex].MinRow / bandHeight; - int endBand = edges[edgeIndex].MaxRow / bandHeight; - for (int b = startBand; b <= endBand; b++) + // Reuse the caller-provided scratch when dimensions match; create a new one otherwise. + if (reusableScratch == null || !reusableScratch.CanReuse(wordsPerRow, coverStrideInt, width, bandHeight)) { - bandEdgeReferences[bandWriteCursor[b]++] = edgeIndex; + reusableScratch?.Dispose(); + reusableScratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); } - } - using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); - for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) - { - int bandTop = bandIndex * bandHeight; - int currentBandHeight = Math.Min(bandHeight, height - bandTop); - int start = bandOffsets[bandIndex]; - int length = bandOffsets[bandIndex + 1] - start; - if (length == 0) + WorkerScratch scratch = reusableScratch; + for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) { - // No edge crosses this band, so there is nothing to rasterize or clear. - continue; - } + int bandTop = bandIndex * bandHeight; + int currentBandHeight = Math.Min(bandHeight, height - bandTop); + int start = bandOffsets[bandIndex]; + int length = bandOffsets[bandIndex + 1] - start; + if (length == 0) + { + // No edge crosses this band, so there is nothing to rasterize or clear. + continue; + } + + Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); + Span bandEdges = bandEdgeReferences.Slice(start, length); + if (length >= EdgeIndexSortThreshold) + { + // Sorting edge indices into ascending order improves cache locality when + // accessing the shared edges array: sequential indices → sequential reads. + bandEdges.Sort(); + } - Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); - ReadOnlySpan bandEdges = bandEdgeReferences.Slice(start, length); - context.RasterizeEdgeTable(edges, bandEdges, bandTop); - context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); - context.ResetTouchedRows(); + context.RasterizeEdgeTable(edges, bandEdges, bandTop); + context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } } } @@ -275,6 +315,7 @@ private static void RasterizeSequentialBands( /// Coverage mode (AA or aliased). /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. + /// Caller-managed scratch. Reused when compatible; replaced and updated in place otherwise. /// /// when the tiled path executed successfully; /// when the caller should run sequential fallback. @@ -291,7 +332,8 @@ private static bool TryRasterizeParallel( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, MemoryAllocator allocator, - RasterizerCoverageRowHandler rowHandler) + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch) { int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); if (tileHeight < 1) @@ -314,7 +356,8 @@ private static bool TryRasterizeParallel( intersectionRule, rasterizationMode, allocator, - rowHandler); + rowHandler, + ref reusableScratch); return true; } @@ -324,109 +367,75 @@ private static bool TryRasterizeParallel( return false; } - using IMemoryOwner tileCountsOwner = allocator.Allocate(tileCount, AllocationOptions.Clean); - Span tileCounts = tileCountsOwner.Memory.Span; - - long totalTileEdgeReferences = 0; - Span edgeBuffer = edgeMemory.Span; - for (int i = 0; i < edgeCount; i++) + if (!TryBuildEdgeBuckets( + edgeMemory.Span[..edgeCount], + tileCount, + tileHeight, + allocator, + out IMemoryOwner tileOffsetsOwner, + out IMemoryOwner tileEdgeReferencesOwner)) { - // Same CSR construction as sequential mode, now keyed by tile instead of band. - int startTile = edgeBuffer[i].MinRow / tileHeight; - int endTile = edgeBuffer[i].MaxRow / tileHeight; - int tileSpan = (endTile - startTile) + 1; - totalTileEdgeReferences += tileSpan; - - if (totalTileEdgeReferences > int.MaxValue) - { - return false; - } - - for (int t = startTile; t <= endTile; t++) - { - tileCounts[t]++; - } + return false; } - int totalReferences = (int)totalTileEdgeReferences; - using IMemoryOwner tileOffsetsOwner = allocator.Allocate(tileCount + 1); - Memory tileOffsetsMemory = tileOffsetsOwner.Memory; - Span tileOffsets = tileOffsetsMemory.Span; - - int offset = 0; - for (int t = 0; t < tileCount; t++) + using (tileOffsetsOwner) + using (tileEdgeReferencesOwner) { - // Prefix sum over tile counts so each tile gets one contiguous slice. - tileOffsets[t] = offset; - offset += tileCounts[t]; - } - - tileOffsets[tileCount] = offset; - using IMemoryOwner tileWriteCursorOwner = allocator.Allocate(tileCount); - Span tileWriteCursor = tileWriteCursorOwner.Memory.Span; - tileOffsets[..tileCount].CopyTo(tileWriteCursor); + Memory tileOffsetsMemory = tileOffsetsOwner.Memory; + Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; - using IMemoryOwner tileEdgeReferencesOwner = allocator.Allocate(totalReferences); - Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - - for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) - { - int startTile = edgeBuffer[edgeIndex].MinRow / tileHeight; - int endTile = edgeBuffer[edgeIndex].MaxRow / tileHeight; - for (int t = startTile; t <= endTile; t++) + ParallelOptions parallelOptions = new() { - // Scatter edge indices into each tile's contiguous bucket. - tileEdgeReferences[tileWriteCursor[t]++] = edgeIndex; - } - } + MaxDegreeOfParallelism = Math.Min(MaxParallelWorkerCount, Math.Min(Environment.ProcessorCount, tileCount)) + }; - ParallelOptions parallelOptions = new() - { - MaxDegreeOfParallelism = Math.Min(MaxParallelWorkerCount, Math.Min(Environment.ProcessorCount, tileCount)) - }; - - _ = Parallel.For( - 0, - tileCount, - parallelOptions, - () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), - (tileIndex, _, worker) => - { - Context context = default; - bool hasCoverage = false; - int tile = tileIndex; - int bandTop = tile * tileHeight; - try + _ = Parallel.For( + 0, + tileCount, + parallelOptions, + () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), + (tileIndex, _, worker) => { - ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; - Span tileOffsets = tileOffsetsMemory.Span; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - int bandHeight = Math.Min(tileHeight, height - bandTop); - int start = tileOffsets[tile]; - int length = tileOffsets[tile + 1] - start; - if (length > 0) + Context context = default; + bool hasCoverage = false; + int tile = tileIndex; + int bandTop = tile * tileHeight; + try { - ReadOnlySpan tileEdges = tileEdgeReferences.Slice(start, length); - context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, tileEdges, bandTop); - hasCoverage = true; - context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); + ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; + Span tileOffsets = tileOffsetsMemory.Span; + Span tileEdgeReferences = tileEdgeReferencesMemory.Span; + int bandHeight = Math.Min(tileHeight, height - bandTop); + int start = tileOffsets[tile]; + int length = tileOffsets[tile + 1] - start; + if (length > 0) + { + Span tileEdges = tileEdgeReferences.Slice(start, length); + if (length >= EdgeIndexSortThreshold) + { + tileEdges.Sort(); + } + + context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, tileEdges, bandTop); + hasCoverage = true; + context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); + } } - } - finally - { - if (hasCoverage) + finally { - context.ResetTouchedRows(); + if (hasCoverage) + { + context.ResetTouchedRows(); + } } - } - return worker; - }, - static worker => worker.Dispose()); + return worker; + }, + static worker => worker.Dispose()); - return true; + return true; + } } /// @@ -446,6 +455,10 @@ private static bool TryRasterizeParallel( /// Coverage mode (AA or aliased). /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. + /// + /// Caller-managed scratch. Reused when compatible; replaced and updated in place otherwise. + /// The caller owns the lifetime and must dispose after the last use. + /// private static void RasterizeSingleTileDirect( ReadOnlySpan edges, int width, @@ -456,15 +469,101 @@ private static void RasterizeSingleTileDirect( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, MemoryAllocator allocator, - RasterizerCoverageRowHandler rowHandler) + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch) { - using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); + // Reuse the caller-provided scratch when dimensions match; create a new one otherwise. + if (reusableScratch == null || !reusableScratch.CanReuse(wordsPerRow, coverStride, width, height)) + { + reusableScratch?.Dispose(); + reusableScratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); + } + + WorkerScratch scratch = reusableScratch; Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); context.RasterizeEdgeTable(edges, bandTop: 0); context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); context.ResetTouchedRows(); } + /// + /// Builds a CSR (Compressed Sparse Row) edge-to-bucket index mapping. + /// Each edge is recorded in every bucket whose row range it overlaps. + /// + /// The prebuilt edge table. + /// Total number of buckets. + /// Rows per bucket. + /// Temporary buffer allocator. + /// + /// On success, receives an owned buffer of length + 1 containing + /// the CSR row offsets (prefix sums). Caller is responsible for disposal. + /// + /// + /// On success, receives an owned buffer containing edge index references. Caller is responsible for disposal. + /// + /// + /// on success; + /// if the total reference count would overflow . + /// + private static bool TryBuildEdgeBuckets( + ReadOnlySpan edges, + int bucketCount, + int bucketHeight, + MemoryAllocator allocator, + out IMemoryOwner offsetsOwner, + out IMemoryOwner referencesOwner) + { + using IMemoryOwner countsOwner = allocator.Allocate(bucketCount, AllocationOptions.Clean); + Span counts = countsOwner.Memory.Span; + long totalRefs = 0; + for (int i = 0; i < edges.Length; i++) + { + int startBucket = edges[i].MinRow / bucketHeight; + int endBucket = edges[i].MaxRow / bucketHeight; + totalRefs += (endBucket - startBucket) + 1; + if (totalRefs > int.MaxValue) + { + offsetsOwner = null!; + referencesOwner = null!; + return false; + } + + for (int b = startBucket; b <= endBucket; b++) + { + counts[b]++; + } + } + + int totalReferences = (int)totalRefs; + offsetsOwner = allocator.Allocate(bucketCount + 1); + Span offsets = offsetsOwner.Memory.Span; + int offset = 0; + for (int b = 0; b < bucketCount; b++) + { + offsets[b] = offset; + offset += counts[b]; + } + + offsets[bucketCount] = offset; + using IMemoryOwner writeCursorOwner = allocator.Allocate(bucketCount); + Span writeCursor = writeCursorOwner.Memory.Span; + offsets[..bucketCount].CopyTo(writeCursor); + + referencesOwner = allocator.Allocate(totalReferences); + Span references = referencesOwner.Memory.Span; + for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) + { + int startBucket = edges[edgeIndex].MinRow / bucketHeight; + int endBucket = edges[edgeIndex].MaxRow / bucketHeight; + for (int b = startBucket; b <= endBucket; b++) + { + references[writeCursor[b]++] = edgeIndex; + } + } + + return true; + } + /// /// Builds an edge table in scanner-local coordinates. /// @@ -805,7 +904,7 @@ private static void ThrowBandHeightExceedsScratchCapacity() /// /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. /// - private ref struct Context + internal ref struct Context { private readonly Span bitVectors; private readonly Span coverArea; @@ -929,6 +1028,15 @@ public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) int x1 = edge.X1; int y1 = edge.Y1; + // Fast-path: edge is fully within this band — no clipping needed. + // MinRow >= bandTop guarantees min(y0,y1) >= bandTopFixed. + // MaxRow < bandTop + height guarantees max(y0,y1) < bandBottomFixed. + if (edge.MinRow >= bandTop && edge.MaxRow < bandTop + this.height) + { + this.RasterizeLine(x0, y0 - bandTopFixed, x1, y1 - bandTopFixed); + continue; + } + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) { continue; @@ -961,6 +1069,15 @@ public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan e int x1 = edge.X1; int y1 = edge.Y1; + // Fast-path: edge is fully within this band — no clipping needed. + // MinRow >= bandTop guarantees min(y0,y1) >= bandTopFixed. + // MaxRow < bandTop + height guarantees max(y0,y1) < bandBottomFixed. + if (edge.MinRow >= bandTop && edge.MaxRow < bandTop + this.height) + { + this.RasterizeLine(x0, y0 - bandTopFixed, x1, y1 - bandTopFixed); + continue; + } + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) { continue; @@ -982,13 +1099,17 @@ public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan e /// Coverage callback invoked for each emitted non-zero span. public readonly void EmitCoverageRows(int destinationTop, Span scanline, RasterizerCoverageRowHandler rowHandler) { - for (int row = 0; row < this.height; row++) + // Iterate only rows that actually received coverage contributions. + // MarkRowTouched is called from AddCell for all contributions, including + // column-less startCover accumulations, so touchedRows is complete. + for (int i = 0; i < this.touchedRowCount; i++) { + int row = this.touchedRows[i]; int rowCover = this.startCover[row]; bool rowHasBits = this.rowHasBits[row] != 0; if (rowCover == 0 && !rowHasBits) { - // Nothing contributed to this row. + // Safety guard — should not fire in practice. continue; } @@ -1290,7 +1411,7 @@ private static void EmitRun( /// Sets a row/column bit and reports whether it was newly set. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly bool ConditionalSetBit(int row, int column) + private readonly bool ConditionalSetBit(int row, int column, out bool rowHadBits) { int bitIndex = row * this.wordsPerRow; int wordIndex = bitIndex + (column / WordBitCount); @@ -1299,8 +1420,14 @@ private readonly bool ConditionalSetBit(int row, int column) bool newlySet = (word & mask) == 0; word |= mask; - // Fast row-level early-out for EmitCoverageRows. - this.rowHasBits[row] = 1; + // Single read of rowHasBits serves both the conditional store + // and the caller's min/max column tracking. + rowHadBits = this.rowHasBits[row] != 0; + if (!rowHadBits) + { + this.rowHasBits[row] = 1; + } + return newlySet; } @@ -1330,8 +1457,7 @@ private void AddCell(int row, int column, int delta, int area) } int index = (row * this.coverStride) + (column << 1); - bool rowHadBits = this.rowHasBits[row] != 0; - if (this.ConditionalSetBit(row, column)) + if (this.ConditionalSetBit(row, column, out bool rowHadBits)) { // First write wins initialization path avoids reading old values. this.coverArea[index] = delta; @@ -1991,7 +2117,7 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path /// access without per-read unpacking. /// - private readonly struct EdgeData + internal readonly struct EdgeData { /// /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). @@ -2040,7 +2166,7 @@ public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) /// /// Reusable per-worker scratch buffers used by tiled and sequential band rasterization. /// - private sealed class WorkerScratch : IDisposable + internal sealed class WorkerScratch : IDisposable { private readonly int wordsPerRow; private readonly int coverStride; @@ -2091,6 +2217,16 @@ private WorkerScratch( /// public Span Scanline => this.scanlineOwner.Memory.Span; + /// + /// Returns when this scratch has compatible dimensions and sufficient + /// capacity for the requested parameters, making it safe to reuse without reallocation. + /// + internal bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int requiredWidth, int minCapacity) + => this.wordsPerRow == requiredWordsPerRow + && this.coverStride == requiredCoverStride + && this.width == requiredWidth + && this.tileCapacity >= minCapacity; + /// /// Allocates worker-local scratch sized for the configured tile/band capacity. /// From 7d50cf92deb1058e6a10476f4b72315387ad0b61 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 19:16:02 +1000 Subject: [PATCH 085/136] Fix tile mapping and dispatch dimensions --- .../Shaders/PreparedCompositeFineComputeShader.cs | 14 ++++++++------ .../WebGPUDrawingBackend.cs | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs index 9744e14e6..f4873554e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs @@ -200,19 +200,21 @@ fn positive_mod(value: i32, divisor: i32) -> i32 { @compute @workgroup_size(8, 8, 1) fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let tile_index = global_id.z; + let tile_x = global_id.z; + let tile_y = global_id.y / 16u; + let tile_index = tile_y * dispatch_config.tile_count_x + tile_x; if (tile_index >= dispatch_config.tile_count) { return; } - if (global_id.x >= 16u || global_id.y >= 16u) { + let pixel_x = global_id.x; + let pixel_y = global_id.y % 16u; + if (pixel_x >= 16u || pixel_y >= 16u) { return; } - let tile_x = tile_index % dispatch_config.tile_count_x; - let tile_y = tile_index / dispatch_config.tile_count_x; - let dest_x = (tile_x * 16u) + global_id.x; - let dest_y = (tile_y * 16u) + global_id.y; + let dest_x = (tile_x * 16u) + pixel_x; + let dest_y = (tile_y * 16u) + pixel_y; if (dest_x >= dispatch_config.target_width || dest_y >= dispatch_config.target_height) { return; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 2d7956485..fc80958d7 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -1108,8 +1108,8 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out flushContext.Api.ComputePassEncoderDispatchWorkgroups( passEncoder, DivideRoundUp(CompositeTileWidth, CompositeComputeWorkgroupSize), - DivideRoundUp(CompositeTileHeight, CompositeComputeWorkgroupSize), - (uint)tileCount); + DivideRoundUp(CompositeTileHeight, CompositeComputeWorkgroupSize) * (uint)tileCountY, + (uint)tileCountX); } finally { From 6fbc48178e3ed1c41e4fdcd1676b5fbeaf329941 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Mar 2026 20:14:32 +1000 Subject: [PATCH 086/136] Use output texture for readback to avoid copy --- .../WebGPUDrawingBackend.cs | 93 ++++++------------- .../WebGPUFlushContext.cs | 7 ++ 2 files changed, 36 insertions(+), 64 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index fc80958d7..22572a57c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -437,75 +437,26 @@ private bool TryRenderPreparedFlush( return false; } + // Use the target texture directly as the backdrop source. + // This avoids an extra texture allocation and target→source copy. TextureView* backdropTextureView = flushContext.TargetView; int sourceOriginX = targetLocalBounds.X; int sourceOriginY = targetLocalBounds.Y; - Texture* outputTexture = flushContext.TargetTexture; - TextureView* outputTextureView = flushContext.TargetView; - bool writesDirectlyToTarget = !flushContext.RequiresReadback; - bool copyOutputToTarget = !writesDirectlyToTarget; - int outputOriginX = writesDirectlyToTarget ? targetLocalBounds.X : 0; - int outputOriginY = writesDirectlyToTarget ? targetLocalBounds.Y : 0; - if (writesDirectlyToTarget) - { - backdropTextureView = flushContext.TargetView; - sourceOriginX = targetLocalBounds.X; - sourceOriginY = targetLocalBounds.Y; - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out outputTexture, - out outputTextureView, - out error)) - { - return false; - } - - outputOriginX = 0; - outputOriginY = 0; - copyOutputToTarget = true; - } - else - { - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out Texture* sourceTexture, - out backdropTextureView, - out error)) - { - return false; - } - CopyTextureRegion( + if (!TryCreateCompositionTexture( flushContext, - flushContext.TargetTexture, - targetLocalBounds.X, - targetLocalBounds.Y, - sourceTexture, - 0, - 0, targetLocalBounds.Width, - targetLocalBounds.Height); - sourceOriginX = 0; - sourceOriginY = 0; - if (!TryCreateCompositionTexture( - flushContext, - targetLocalBounds.Width, - targetLocalBounds.Height, - out outputTexture, - out outputTextureView, - out error)) - { - return false; - } - - outputOriginX = 0; - outputOriginY = 0; + targetLocalBounds.Height, + out Texture* outputTexture, + out TextureView* outputTextureView, + out error)) + { + return false; } + int outputOriginX = 0; + int outputOriginY = 0; + List coverageDefinitions = []; Dictionary coverageDefinitionIndexByKey = []; int[] batchCoverageIndices = new int[preparedBatches.Count]; @@ -572,8 +523,15 @@ private bool TryRenderPreparedFlush( return false; } - if (copyOutputToTarget) + if (flushContext.RequiresReadback) { + // CPU target: read back directly from the output texture at (0,0) + // instead of copying output→target and then reading from target. + flushContext.ReadbackSourceOverride = outputTexture; + } + else + { + // Native GPU surface: copy composited output back into the target. CopyTextureRegion( flushContext, outputTexture, @@ -1960,11 +1918,18 @@ flushContext.ReadbackBuffer is null || uint copyBytesPerRow = checked((uint)copyBounds.Width * (uint)Unsafe.SizeOf()); copyBytesPerRow = (copyBytesPerRow + 255U) & ~255U; + // When ReadbackSourceOverride is set, the output texture already contains the + // composited result at (0,0), so we read from there instead of the target texture. + bool useOverride = flushContext.ReadbackSourceOverride is not null; + Texture* readbackTexture = useOverride ? flushContext.ReadbackSourceOverride : flushContext.TargetTexture; + uint readbackOriginX = useOverride ? 0 : (uint)copyBounds.X; + uint readbackOriginY = useOverride ? 0 : (uint)copyBounds.Y; + ImageCopyTexture source = new() { - Texture = flushContext.TargetTexture, + Texture = readbackTexture, MipLevel = 0, - Origin = new Origin3D((uint)copyBounds.X, (uint)copyBounds.Y, 0), + Origin = new Origin3D(readbackOriginX, readbackOriginY, 0), Aspect = TextureAspect.All }; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 2b1bc3201..954124622 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -122,6 +122,13 @@ private WebGPUFlushContext( /// public bool RequiresReadback { get; private set; } + /// + /// Gets or sets an optional override texture to read back from instead of . + /// When set, readback copies from this texture at origin (0,0) rather than from the target + /// at composition bounds, eliminating an intermediate texture-to-texture copy. + /// + public Texture* ReadbackSourceOverride { get; set; } + /// /// Gets a value indicating whether the current target texture can be sampled in a compute shader. /// From 0d6f935cd5ce84ff9ca1d88074b376a49fd19b4d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 11:59:09 +1000 Subject: [PATCH 087/136] Add CSR shaders; restructure WebGPU shaders & tests --- .../Shaders/BackdropComputeShader.cs | 118 -- .../Shaders/CoverageFineComputeShader.cs | 157 -- .../Shaders/CsrCountComputeShader.cs | 57 + .../Shaders/CsrScatterComputeShader.cs | 63 + .../Shaders/PathCountComputeShader.cs | 260 --- .../Shaders/PathCountSetupComputeShader.cs | 68 - .../Shaders/PathTilingComputeShader.cs | 217 --- .../Shaders/PathTilingSetupComputeShader.cs | 68 - .../PreparedCompositeFineComputeShader.cs | 747 +++++++- ...mpositeTilePrefixBlockScanComputeShader.cs | 98 + ...edCompositeTilePrefixLocalComputeShader.cs | 101 + ...mpositeTilePrefixPropagateComputeShader.cs | 59 + .../Shaders/SegmentAllocComputeShader.cs | 80 - ...WebGPUDrawingBackend.CoverageRasterizer.cs | 1685 +++-------------- .../WebGPUDrawingBackend.cs | 817 +++++++- .../WebGPUFlushContext.cs | 4 +- .../Backends/WebGPUDrawingBackendTests.cs | 353 +++- ...sDefaultOutput_DrawPath_Stroke_Default.png | 3 + ...utput_DrawPath_Stroke_WebGPU_CPURegion.png | 3 + ...t_DrawPath_Stroke_WebGPU_NativeSurface.png | 3 + ...atedGlyphs_AfterClear_WebGPU_CPURegion.png | 4 +- ...Glyphs_AfterClear_WebGPU_NativeSurface.png | 4 +- ...eCache_RepeatedGlyphs_WebGPU_CPURegion.png | 4 +- ...he_RepeatedGlyphs_WebGPU_NativeSurface.png | 4 +- ...aredCoverage_DrawText_WebGPU_CPURegion.png | 4 +- ...Coverage_DrawText_WebGPU_NativeSurface.png | 4 +- ...DefaultOutput_FillPath_EvenOdd_Default.png | 3 + ...tput_FillPath_EvenOdd_WebGPU_CPURegion.png | 3 + ..._FillPath_EvenOdd_WebGPU_NativeSurface.png | 3 + ...Output_FillPath_LargeTileCount_Default.png | 3 + ...llPath_LargeTileCount_WebGPU_CPURegion.png | 3 + ...th_LargeTileCount_WebGPU_NativeSurface.png | 3 + ...tput_FillPath_MultipleSeparate_Default.png | 3 + ...Path_MultipleSeparate_WebGPU_CPURegion.png | 3 + ..._MultipleSeparate_WebGPU_NativeSurface.png | 3 + ...t_FillPath_ImageBrush_WebGPU_CPURegion.png | 2 +- ...llPath_ImageBrush_WebGPU_NativeSurface.png | 2 +- ...h_NativeSurfaceParity_WebGPU_CPURegion.png | 4 +- ...tiveSurfaceParity_WebGPU_NativeSurface.png | 4 +- ...CorrectResults_MultipleFlushes_Default.png | 3 + ...sults_MultipleFlushes_WebGPU_CPURegion.png | 3 + ...s_MultipleFlushes_WebGPU_NativeSurface.png | 3 + ...DefaultOutput_Process_WebGPU_CPURegion.png | 4 +- ...ultOutput_Process_WebGPU_NativeSurface.png | 4 +- 44 files changed, 2489 insertions(+), 2552 deletions(-) delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CsrCountComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CsrScatterComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs deleted file mode 100644 index fd1043d98..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Copies the destination texture into a composition backdrop for read-only sampling. -/// -internal static class BackdropComputeShader -{ - /// - /// Gets the null-terminated WGSL source for the backdrop copy pass. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - struct Tile { - backdrop: i32, - segment_count_or_ix: u32, - } - - struct Config { - width_in_tiles: u32, - height_in_tiles: u32, - target_width: u32, - target_height: u32, - base_color: u32, - n_drawobj: u32, - n_path: u32, - n_clip: u32, - bin_data_start: u32, - pathtag_base: u32, - pathdata_base: u32, - drawtag_base: u32, - drawdata_base: u32, - transform_base: u32, - style_base: u32, - lines_size: u32, - binning_size: u32, - tiles_size: u32, - seg_counts_size: u32, - segments_size: u32, - blend_size: u32, - ptcl_size: u32, - } - - @group(0) @binding(0) - var config: Config; - - @group(0) @binding(1) - var tiles: array; - - const WG_SIZE = 64u; - var sh_backdrop: array; - var running_backdrop: i32; - - @compute @workgroup_size(64) - fn cs_main( - @builtin(local_invocation_id) local_id: vec3, - @builtin(workgroup_id) wg_id: vec3, - ) { - let width_in_tiles = config.width_in_tiles; - let row_index = wg_id.x; - if row_index >= config.height_in_tiles { - return; - } - - if local_id.x == 0u { - running_backdrop = 0; - } - workgroupBarrier(); - - var chunk_start = 0u; - loop { - if chunk_start >= width_in_tiles { - break; - } - - let count = min(WG_SIZE, width_in_tiles - chunk_start); - var backdrop = 0; - if local_id.x < count { - let ix = row_index * width_in_tiles + chunk_start + local_id.x; - backdrop = tiles[ix].backdrop; - } - - sh_backdrop[local_id.x] = backdrop; - for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { - workgroupBarrier(); - if local_id.x >= (1u << i) { - backdrop += sh_backdrop[local_id.x - (1u << i)]; - } - - workgroupBarrier(); - sh_backdrop[local_id.x] = backdrop; - } - - workgroupBarrier(); - if local_id.x < count { - let ix = row_index * width_in_tiles + chunk_start + local_id.x; - let accumulated = sh_backdrop[local_id.x] + running_backdrop; - tiles[ix].backdrop = accumulated; - if local_id.x + 1u == count { - running_backdrop = accumulated; - } - } - - workgroupBarrier(); - chunk_start += WG_SIZE; - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs deleted file mode 100644 index af2c99da4..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CoverageFineComputeShader.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Computes fine coverage tiles from the accumulated segment data. -/// -internal static class CoverageFineComputeShader -{ - /// - /// Gets the null-terminated WGSL source for the coverage fine rasterization pass. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Coverage fine rasterizer (adapted from fine.wgsl). - - const TILE_WIDTH = 16u; - const TILE_HEIGHT = 16u; - - struct Segment { - point0: vec2, - point1: vec2, - y_edge: f32, - } - - struct Tile { - backdrop: i32, - segment_count_or_ix: u32, - } - - struct CoverageParams { - target_width: u32, - target_height: u32, - tile_origin_x: u32, - tile_origin_y: u32, - tile_width_in_tiles: u32, - tile_height_in_tiles: u32, - fill_rule: u32, - is_aliased: u32, - } - - @group(0) @binding(0) - var params: CoverageParams; - - @group(0) @binding(1) - var tiles: array; - - @group(0) @binding(2) - var tile_counts: array; - - @group(0) @binding(3) - var segments: array; - - @group(0) @binding(4) - var output: texture_storage_2d; - - const PIXELS_PER_THREAD = 4u; - - fn fill_path(seg_offset: u32, seg_count: u32, backdrop: i32, even_odd: bool, xy: vec2, result: ptr>) { - var area: array; - let backdrop_f = f32(backdrop); - for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - area[i] = backdrop_f; - } - for (var i = 0u; i < seg_count; i++) { - let seg_off = seg_offset + i; - let segment = segments[seg_off]; - let y = segment.point0.y - xy.y; - let delta = segment.point1 - segment.point0; - let y0 = clamp(y, 0.0, 1.0); - let y1 = clamp(y + delta.y, 0.0, 1.0); - let dy = y0 - y1; - if dy != 0.0 { - let vec_y_recip = 1.0 / delta.y; - let t0 = (y0 - y) * vec_y_recip; - let t1 = (y1 - y) * vec_y_recip; - let startx = segment.point0.x - xy.x; - let x0 = startx + t0 * delta.x; - let x1 = startx + t1 * delta.x; - let xmin0 = min(x0, x1); - let xmax0 = max(x0, x1); - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - let j_f = f32(j); - let xmin = min(xmin0 - j_f, 1.0) - 1.0e-6; - let xmax = xmax0 - j_f; - let b = min(xmax, 1.0); - let c = max(b, 0.0); - let d = max(xmin, 0.0); - let a = (b + 0.5 * (d * d - c * c) - xmin) / (xmax - xmin); - area[j] += a * dy; - } - } - let y_edge = sign(delta.x) * clamp(xy.y - segment.y_edge + 1.0, 0.0, 1.0); - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - area[j] += y_edge; - } - } - if even_odd { - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - let a = area[j]; - area[j] = abs(a - 2.0 * round(0.5 * a)); - } - } else { - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - area[j] = min(abs(area[j]), 1.0); - } - } - *result = area; - } - - @compute @workgroup_size(4, 16) - fn cs_main( - @builtin(global_invocation_id) global_id: vec3, - @builtin(local_invocation_id) local_id: vec3, - @builtin(workgroup_id) wg_id: vec3, - ) { - let tile_ix = wg_id.y * params.tile_width_in_tiles + wg_id.x; - if tile_ix >= params.tile_width_in_tiles * params.tile_height_in_tiles { - return; - } - - let seg_count = tile_counts[tile_ix]; - let tile = tiles[tile_ix]; - let seg_offset = ~tile.segment_count_or_ix; - let even_odd = params.fill_rule != 0u; - - let local_xy = vec2(f32(local_id.x * PIXELS_PER_THREAD), f32(local_id.y)); - var area: array; - fill_path(seg_offset, seg_count, tile.backdrop, even_odd, local_xy, &area); - - if params.is_aliased != 0u { - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - area[j] = select(0.0, 1.0, area[j] >= 0.5); - } - } - - let pixel_base = vec2( - params.tile_origin_x * TILE_WIDTH + global_id.x * PIXELS_PER_THREAD, - params.tile_origin_y * TILE_HEIGHT + global_id.y); - - for (var j = 0u; j < PIXELS_PER_THREAD; j += 1u) { - let coords = pixel_base + vec2(j, 0u); - if coords.x < params.target_width && coords.y < params.target_height { - textureStore(output, vec2(coords), vec4(area[j], 0.0, 0.0, 0.0)); - } - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CsrCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrCountComputeShader.cs new file mode 100644 index 000000000..75f7045ad --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrCountComputeShader.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU compute shader that counts edges per CSR band. +/// Each thread processes one edge and atomically increments band counts +/// for each 16-row band the edge overlaps. +/// +internal static class CsrCountComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct Edge { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + min_row: i32, + max_row: i32, + csr_band_offset: u32, + definition_edge_start: u32, + } + + struct CsrConfig { + total_edge_count: u32, + }; + + @group(0) @binding(0) var edges: array; + @group(0) @binding(1) var band_counts: array>; + @group(0) @binding(2) var config: CsrConfig; + + @compute @workgroup_size(256, 1, 1) + fn cs_main(@builtin(global_invocation_id) gid: vec3) { + let edge_idx = gid.x; + if (edge_idx >= config.total_edge_count) { + return; + } + let edge = edges[edge_idx]; + if (edge.min_row > edge.max_row) { + return; + } + let min_band = edge.min_row / 16; + let max_band = edge.max_row / 16; + for (var band = min_band; band <= max_band; band++) { + atomicAdd(&band_counts[edge.csr_band_offset + u32(band)], 1u); + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CsrScatterComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrScatterComputeShader.cs new file mode 100644 index 000000000..b3ab2b409 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrScatterComputeShader.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU compute shader that scatters edge indices into CSR buckets. +/// Each thread processes one edge. For each band the edge overlaps, +/// it atomically claims a slot in csr_indices via a write cursor. +/// +internal static class CsrScatterComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct Edge { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + min_row: i32, + max_row: i32, + csr_band_offset: u32, + definition_edge_start: u32, + } + + struct CsrConfig { + total_edge_count: u32, + }; + + @group(0) @binding(0) var edges: array; + @group(0) @binding(1) var csr_offsets: array; + @group(0) @binding(2) var write_cursors: array>; + @group(0) @binding(3) var csr_indices: array; + @group(0) @binding(4) var config: CsrConfig; + + @compute @workgroup_size(256, 1, 1) + fn cs_main(@builtin(global_invocation_id) gid: vec3) { + let edge_idx = gid.x; + if (edge_idx >= config.total_edge_count) { + return; + } + let edge = edges[edge_idx]; + if (edge.min_row > edge.max_row) { + return; + } + let local_idx = edge_idx - edge.definition_edge_start; + let min_band = edge.min_row / 16; + let max_band = edge.max_row / 16; + for (var band = min_band; band <= max_band; band++) { + let band_offset = edge.csr_band_offset + u32(band); + let offset = csr_offsets[band_offset]; + let slot = atomicAdd(&write_cursors[band_offset], 1u); + csr_indices[offset + slot] = local_idx; + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs deleted file mode 100644 index ad717240e..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Counts paths per tile to size tile command lists. -/// -internal static class PathCountComputeShader -{ - /// - /// Gets the null-terminated WGSL source for the path count pass. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Path count stage. - - const STAGE_BINNING: u32 = 0x1u; - const STAGE_TILE_ALLOC: u32 = 0x2u; - const STAGE_FLATTEN: u32 = 0x4u; - const STAGE_PATH_COUNT: u32 = 0x8u; - const STAGE_COARSE: u32 = 0x10u; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - ptcl: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend: atomic, - lines: atomic, - } - - struct Config { - width_in_tiles: u32, - height_in_tiles: u32, - target_width: u32, - target_height: u32, - base_color: u32, - n_drawobj: u32, - n_path: u32, - n_clip: u32, - bin_data_start: u32, - pathtag_base: u32, - pathdata_base: u32, - drawtag_base: u32, - drawdata_base: u32, - transform_base: u32, - style_base: u32, - lines_size: u32, - binning_size: u32, - tiles_size: u32, - seg_counts_size: u32, - segments_size: u32, - blend_size: u32, - ptcl_size: u32, - } - - const TILE_WIDTH = 16u; - const TILE_HEIGHT = 16u; - const TILE_SCALE = 0.0625; - - struct LineSoup { - path_ix: u32, - p0: vec2, - p1: vec2, - } - - struct SegmentCount { - line_ix: u32, - counts: u32, - } - - struct Path { - bbox: vec4, - tiles: u32, - } - - struct Tile { - backdrop: i32, - segment_count_or_ix: u32, - } - - // TODO: this is cut'n'pasted from path_coarse. - struct AtomicTile { - backdrop: atomic, - segment_count_or_ix: atomic, - } - - @group(0) @binding(0) - var config: Config; - - @group(0) @binding(1) - var bump: BumpAllocators; - - @group(0) @binding(2) - var lines: array; - - @group(0) @binding(3) - var paths: array; - - @group(0) @binding(4) - var tile: array; - - @group(0) @binding(5) - var seg_counts: array; - - fn span(a: f32, b: f32) -> u32 { - return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); - } - - const ONE_MINUS_ULP: f32 = 0.99999994; - const ROBUST_EPSILON: f32 = 2e-7; - - @compute @workgroup_size(256) - fn cs_main( - @builtin(global_invocation_id) global_id: vec3, - ) { - let n_lines = atomicLoad(&bump.lines); - var count = 0u; - if global_id.x < n_lines { - let line = lines[global_id.x]; - let is_down = line.p1.y >= line.p0.y; - let xy0 = select(line.p1, line.p0, is_down); - let xy1 = select(line.p0, line.p1, is_down); - let s0 = xy0 * TILE_SCALE; - let s1 = xy1 * TILE_SCALE; - let count_x = span(s0.x, s1.x) - 1u; - count = count_x + span(s0.y, s1.y); - let line_ix = global_id.x; - - let dx = abs(s1.x - s0.x); - let dy = s1.y - s0.y; - if dx + dy == 0.0 { - return; - } - if dy == 0.0 && floor(s0.y) == s0.y { - return; - } - let idxdy = 1.0 / (dx + dy); - var a = dx * idxdy; - let is_positive_slope = s1.x >= s0.x; - let x_sign = select(-1.0, 1.0, is_positive_slope); - let xt0 = floor(s0.x * x_sign); - let c = s0.x * x_sign - xt0; - let y0 = floor(s0.y); - let ytop = select(y0 + 1.0, ceil(s0.y), s0.y == s1.y); - let b = min((dy * c + dx * (ytop - s0.y)) * idxdy, ONE_MINUS_ULP); - let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); - if robust_err != 0.0 { - a -= ROBUST_EPSILON * sign(robust_err); - } - let x0 = xt0 * x_sign + select(-1.0, 0.0, is_positive_slope); - - let path = paths[line.path_ix]; - let bbox = vec4(path.bbox); - let xmin = min(s0.x, s1.x); - let stride = bbox.z - bbox.x; - if s0.y >= f32(bbox.w) || s1.y <= f32(bbox.y) || xmin >= f32(bbox.z) || stride == 0 { - return; - } - var imin = 0u; - if s0.y < f32(bbox.y) { - var iminf = round((f32(bbox.y) - y0 + b - a) / (1.0 - a)) - 1.0; - if y0 + iminf - floor(a * iminf + b) < f32(bbox.y) { - iminf += 1.0; - } - imin = u32(iminf); - } - var imax = count; - if s1.y > f32(bbox.w) { - var imaxf = round((f32(bbox.w) - y0 + b - a) / (1.0 - a)) - 1.0; - if y0 + imaxf - floor(a * imaxf + b) < f32(bbox.w) { - imaxf += 1.0; - } - imax = u32(imaxf); - } - let delta = select(1, -1, is_down); - var ymin = 0; - var ymax = 0; - if max(s0.x, s1.x) <= f32(bbox.x) { - ymin = i32(ceil(s0.y)); - ymax = i32(ceil(s1.y)); - imax = imin; - } else { - let fudge = select(1.0, 0.0, is_positive_slope); - if xmin < f32(bbox.x) { - var f = round((x_sign * (f32(bbox.x) - x0) - b + fudge) / a); - if (x0 + x_sign * floor(a * f + b) < f32(bbox.x)) == is_positive_slope { - f += 1.0; - } - let ynext = i32(y0 + f - floor(a * f + b) + 1.0); - if is_positive_slope { - if u32(f) > imin { - ymin = i32(y0 + select(1.0, 0.0, y0 == s0.y)); - ymax = ynext; - imin = u32(f); - } - } else { - if u32(f) < imax { - ymin = ynext; - ymax = i32(ceil(s1.y)); - imax = u32(f); - } - } - } - if max(s0.x, s1.x) > f32(bbox.z) { - var f = round((x_sign * (f32(bbox.z) - x0) - b + fudge) / a); - if (x0 + x_sign * floor(a * f + b) < f32(bbox.z)) == is_positive_slope { - f += 1.0; - } - if is_positive_slope { - imax = min(imax, u32(f)); - } else { - imin = max(imin, u32(f)); - } - } - } - imax = max(imin, imax); - ymin = max(ymin, bbox.y); - ymax = min(ymax, bbox.w); - for (var y = ymin; y < ymax; y++) { - let base = i32(path.tiles) + (y - bbox.y) * stride; - atomicAdd(&tile[base].backdrop, delta); - } - var last_z = floor(a * (f32(imin) - 1.0) + b); - let seg_base = atomicAdd(&bump.seg_counts, imax - imin); - for (var i = imin; i < imax; i++) { - let subix = i; - let zf = a * f32(subix) + b; - let z = floor(zf); - let y = i32(y0 + f32(subix) - z); - let x = i32(x0 + x_sign * z); - let base = i32(path.tiles) + (y - bbox.y) * stride - bbox.x; - let top_edge = select(last_z == z, y0 == s0.y, subix == 0u); - if top_edge && x + 1 < bbox.z { - let x_bump = max(x + 1, bbox.x); - atomicAdd(&tile[base + x_bump].backdrop, delta); - } - let seg_within_slice = atomicAdd(&tile[base + x].segment_count_or_ix, 1u); - let counts = (seg_within_slice << 16u) | subix; - let seg_count = SegmentCount(line_ix, counts); - let seg_ix = seg_base + i - imin; - if seg_ix < config.seg_counts_size { - seg_counts[seg_ix] = seg_count; - } - last_z = z; - } - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs deleted file mode 100644 index 5c22c329e..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Prepares path count buffers and resets tile counters for the path count pass. -/// -internal static class PathCountSetupComputeShader -{ - /// - /// Gets the null-terminated WGSL source for path-count dispatch setup. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Path count dispatch setup. - - const STAGE_BINNING: u32 = 0x1u; - const STAGE_TILE_ALLOC: u32 = 0x2u; - const STAGE_FLATTEN: u32 = 0x4u; - const STAGE_PATH_COUNT: u32 = 0x8u; - const STAGE_COARSE: u32 = 0x10u; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - ptcl: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend: atomic, - lines: atomic, - } - - struct IndirectCount { - count_x: u32, - count_y: u32, - count_z: u32, - } - - @group(0) @binding(0) - var bump: BumpAllocators; - - @group(0) @binding(1) - var indirect: IndirectCount; - - const WG_SIZE = 256u; - - @compute @workgroup_size(1) - fn cs_main() { - if atomicLoad(&bump.failed) != 0u { - indirect.count_x = 0u; - } else { - let lines = atomicLoad(&bump.lines); - indirect.count_x = (lines + (WG_SIZE - 1u)) / WG_SIZE; - } - indirect.count_y = 1u; - indirect.count_z = 1u; - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs deleted file mode 100644 index 069d4e51f..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Null-terminated WGSL compute shader for path tiling. -/// -internal static class PathTilingComputeShader -{ - /// - /// Gets the null-terminated WGSL source for the path tiling pass. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Path tiling stage. - - const STAGE_BINNING: u32 = 0x1u; - const STAGE_TILE_ALLOC: u32 = 0x2u; - const STAGE_FLATTEN: u32 = 0x4u; - const STAGE_PATH_COUNT: u32 = 0x8u; - const STAGE_COARSE: u32 = 0x10u; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - ptcl: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend: atomic, - lines: atomic, - } - - const TILE_WIDTH = 16u; - const TILE_HEIGHT = 16u; - const TILE_SCALE = 0.0625; - - struct LineSoup { - path_ix: u32, - p0: vec2, - p1: vec2, - } - - struct SegmentCount { - line_ix: u32, - counts: u32, - } - - struct Path { - bbox: vec4, - tiles: u32, - } - - struct Tile { - backdrop: i32, - segment_count_or_ix: u32, - } - - struct Segment { - point0: vec2, - point1: vec2, - y_edge: f32, - } - - @group(0) @binding(0) - var bump: BumpAllocators; - - @group(0) @binding(1) - var seg_counts: array; - - @group(0) @binding(2) - var lines: array; - - @group(0) @binding(3) - var paths: array; - - @group(0) @binding(4) - var tiles: array; - - @group(0) @binding(5) - var segments: array; - - fn span(a: f32, b: f32) -> u32 { - return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); - } - - const ONE_MINUS_ULP: f32 = 0.99999994; - const ROBUST_EPSILON: f32 = 2e-7; - - @compute @workgroup_size(256) - fn cs_main( - @builtin(global_invocation_id) global_id: vec3, - ) { - let n_segments = atomicLoad(&bump.seg_counts); - if global_id.x < n_segments { - let seg_count = seg_counts[global_id.x]; - let line = lines[seg_count.line_ix]; - let counts = seg_count.counts; - let seg_within_slice = counts >> 16u; - let seg_within_line = counts & 0xffffu; - - let is_down = line.p1.y >= line.p0.y; - var xy0 = select(line.p1, line.p0, is_down); - var xy1 = select(line.p0, line.p1, is_down); - let s0 = xy0 * TILE_SCALE; - let s1 = xy1 * TILE_SCALE; - let count_x = span(s0.x, s1.x) - 1u; - let count = count_x + span(s0.y, s1.y); - let dx = abs(s1.x - s0.x); - let dy = s1.y - s0.y; - let idxdy = 1.0 / (dx + dy); - var a = dx * idxdy; - let is_positive_slope = s1.x >= s0.x; - let x_sign = select(-1.0, 1.0, is_positive_slope); - let xt0 = floor(s0.x * x_sign); - let c = s0.x * x_sign - xt0; - let y0i = floor(s0.y); - let ytop = select(y0i + 1.0, ceil(s0.y), s0.y == s1.y); - let b = min((dy * c + dx * (ytop - s0.y)) * idxdy, ONE_MINUS_ULP); - let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); - if robust_err != 0.0 { - a -= ROBUST_EPSILON * sign(robust_err); - } - let x0i = i32(xt0 * x_sign + 0.5 * (x_sign - 1.0)); - let z = floor(a * f32(seg_within_line) + b); - let x = x0i + i32(x_sign * z); - let y = i32(y0i + f32(seg_within_line) - z); - - let path = paths[line.path_ix]; - let bbox = vec4(path.bbox); - let stride = bbox.z - bbox.x; - let tile_ix = i32(path.tiles) + (y - bbox.y) * stride + x - bbox.x; - let tile = tiles[tile_ix]; - let seg_start = ~tile.segment_count_or_ix; - if i32(seg_start) < 0 { - return; - } - let tile_xy = vec2(f32(x) * f32(TILE_WIDTH), f32(y) * f32(TILE_HEIGHT)); - let tile_xy1 = tile_xy + vec2(f32(TILE_WIDTH), f32(TILE_HEIGHT)); - - if seg_within_line > 0u { - let z_prev = floor(a * (f32(seg_within_line) - 1.0) + b); - if z == z_prev { - var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy.y - xy0.y) / (xy1.y - xy0.y); - xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); - xy0 = vec2(xt, tile_xy.y); - } else { - let x_clip = select(tile_xy1.x, tile_xy.x, is_positive_slope); - var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); - yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); - xy0 = vec2(x_clip, yt); - } - } - if seg_within_line < count - 1u { - let z_next = floor(a * (f32(seg_within_line) + 1.0) + b); - if z == z_next { - var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy1.y - xy0.y) / (xy1.y - xy0.y); - xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); - xy1 = vec2(xt, tile_xy1.y); - } else { - let x_clip = select(tile_xy.x, tile_xy1.x, is_positive_slope); - var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); - yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); - xy1 = vec2(x_clip, yt); - } - } - var y_edge = 1e9; - var p0 = xy0 - tile_xy; - var p1 = xy1 - tile_xy; - let EPSILON = 1e-6; - if p0.x == 0.0 { - if p1.x == 0.0 { - p0.x = EPSILON; - if p0.y == 0.0 { - p1.x = EPSILON; - p1.y = f32(TILE_HEIGHT); - } else { - p1.x = 2.0 * EPSILON; - p1.y = p0.y; - } - } else if p0.y == 0.0 { - p0.x = EPSILON; - } else { - y_edge = p0.y; - } - } else if p1.x == 0.0 { - if p1.y == 0.0 { - p1.x = EPSILON; - } else { - y_edge = p1.y; - } - } - if p0.x == floor(p0.x) && p0.x != 0.0 { - p0.x -= EPSILON; - } - if p1.x == floor(p1.x) && p1.x != 0.0 { - p1.x -= EPSILON; - } - if !is_down { - let tmp = p0; - p0 = p1; - p1 = tmp; - } - let segment = Segment(p0, p1, y_edge); - segments[seg_start + seg_within_slice] = segment; - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs deleted file mode 100644 index 2ea18924c..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Initializes path tiling resources before tile generation. -/// -internal static class PathTilingSetupComputeShader -{ - /// - /// Gets the null-terminated WGSL source for path-tiling dispatch setup. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Path tiling dispatch setup. - - const STAGE_BINNING: u32 = 0x1u; - const STAGE_TILE_ALLOC: u32 = 0x2u; - const STAGE_FLATTEN: u32 = 0x4u; - const STAGE_PATH_COUNT: u32 = 0x8u; - const STAGE_COARSE: u32 = 0x10u; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - ptcl: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend: atomic, - lines: atomic, - } - - struct IndirectCount { - count_x: u32, - count_y: u32, - count_z: u32, - } - - @group(0) @binding(0) - var bump: BumpAllocators; - - @group(0) @binding(1) - var indirect: IndirectCount; - - const WG_SIZE = 256u; - - @compute @workgroup_size(1) - fn cs_main() { - if atomicLoad(&bump.failed) != 0u { - indirect.count_x = 0u; - } else { - let segments = atomicLoad(&bump.seg_counts); - indirect.count_x = (segments + (WG_SIZE - 1u)) / WG_SIZE; - } - indirect.count_y = 1u; - indirect.count_z = 1u; - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs index f4873554e..ce8b5c92d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs @@ -8,6 +8,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Composites prepared commands over coverage in tile order to produce the final output. +/// Coverage is computed inline using a fixed-point scanline rasterizer ported from +/// , operating per-tile with workgroup shared memory. /// Shader source is generated per texture format to match sampling/output requirements. /// internal static class PreparedCompositeFineComputeShader @@ -17,14 +19,28 @@ internal static class PreparedCompositeFineComputeShader private static readonly string ShaderTemplate = """ + struct Edge { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + min_row: i32, + max_row: i32, + csr_band_offset: u32, + definition_edge_start: u32, + } + struct Params { destination_x: u32, destination_y: u32, destination_width: u32, destination_height: u32, - coverage_offset_x: u32, - coverage_offset_y: u32, - target_width: u32, + edge_start: u32, + fill_rule_value: u32, + edge_origin_x: i32, + edge_origin_y: i32, + csr_offsets_start: u32, + csr_band_count: u32, brush_type: u32, brush_origin_x: u32, brush_origin_y: u32, @@ -60,7 +76,7 @@ struct DispatchConfig { bin_data_start: u32, }; - @group(0) @binding(0) var coverage_texture: texture_2d; + @group(0) @binding(0) var edges: array; @group(0) @binding(1) var backdrop_texture: texture_2d<__BACKDROP_TEXEL_TYPE__>; @group(0) @binding(2) var brush_texture: texture_2d<__BACKDROP_TEXEL_TYPE__>; @group(0) @binding(3) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; @@ -69,6 +85,22 @@ struct DispatchConfig { @group(0) @binding(6) var tile_counts: array>; @group(0) @binding(7) var tile_command_indices: array; @group(0) @binding(8) var dispatch_config: DispatchConfig; + @group(0) @binding(9) var csr_offsets: array; + @group(0) @binding(10) var csr_indices: array; + + // Workgroup shared memory for per-tile coverage accumulation. + // Layout: 16 rows x 16 columns. Index = row * 16 + col. + var tile_cover: array, 256>; + var tile_area: array, 256>; + var tile_start_cover: array, 16>; + + const FIXED_SHIFT: u32 = 8u; + const FIXED_ONE: i32 = 256; + const AREA_SHIFT: u32 = 9u; + const COV_STEPS: i32 = 256; + const COV_SCALE: f32 = 1.0 / 256.0; + const EO_MASK: i32 = 511; + const EO_PERIOD: i32 = 512; fn u32_to_f32(bits: u32) -> f32 { return bitcast(bits); @@ -198,90 +230,683 @@ fn positive_mod(value: i32, divisor: i32) -> i32 { return select(m + divisor, m, m >= 0); } - @compute @workgroup_size(8, 8, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let tile_x = global_id.z; - let tile_y = global_id.y / 16u; - let tile_index = tile_y * dispatch_config.tile_count_x + tile_x; - if (tile_index >= dispatch_config.tile_count) { + // ----------------------------------------------------------------------- + // Fixed-point scanline rasterizer (ported from DefaultRasterizer) + // ----------------------------------------------------------------------- + + fn find_adjustment(value: i32) -> i32 { + let lte0 = (~((value - 1) >> 31)) & 1; + let div256 = (((value & (FIXED_ONE - 1)) - 1) >> 31) & 1; + return lte0 & div256; + } + + fn add_cell(row: i32, col: i32, delta: i32, a: i32) { + if row < 0 || row >= 16 { + return; + } + if col < 0 { + atomicAdd(&tile_start_cover[row], delta); + return; + } + if col >= 16 { + return; + } + let idx = u32(row) * 16u + u32(col); + atomicAdd(&tile_cover[idx], delta); + atomicAdd(&tile_area[idx], a); + } + + fn cell_vertical(px: i32, py: i32, x: i32, y0: i32, y1: i32) { + let delta = y0 - y1; + let a = delta * ((FIXED_ONE * 2) - x - x); + add_cell(py, px, delta, a); + } + + fn cell(row: i32, px: i32, x0: i32, y0: i32, x1: i32, y1: i32) { + let delta = y0 - y1; + let a = delta * ((FIXED_ONE * 2) - x0 - x1); + add_cell(row, px, delta, a); + } + + fn vertical_down(col_index: i32, y0: i32, y1: i32, x: i32) { + let row0 = y0 >> FIXED_SHIFT; + let row1 = (y1 - 1) >> FIXED_SHIFT; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let fx = x - (col_index << FIXED_SHIFT); + if row0 == row1 { + cell_vertical(col_index, row0, fx, fy0, fy1); + return; + } + cell_vertical(col_index, row0, fx, fy0, FIXED_ONE); + for (var row = row0 + 1; row < row1; row++) { + cell_vertical(col_index, row, fx, 0, FIXED_ONE); + } + cell_vertical(col_index, row1, fx, 0, fy1); + } + + fn vertical_up(col_index: i32, y0: i32, y1: i32, x: i32) { + let row0 = (y0 - 1) >> FIXED_SHIFT; + let row1 = y1 >> FIXED_SHIFT; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let fx = x - (col_index << FIXED_SHIFT); + if row0 == row1 { + cell_vertical(col_index, row0, fx, fy0, fy1); + return; + } + cell_vertical(col_index, row0, fx, fy0, 0); + for (var row = row0 - 1; row > row1; row--) { + cell_vertical(col_index, row, fx, FIXED_ONE, 0); + } + cell_vertical(col_index, row1, fx, FIXED_ONE, fy1); + } + + fn row_down_r(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + let col0 = p0x >> FIXED_SHIFT; + let col1 = (p1x - 1) >> FIXED_SHIFT; + let fx0 = p0x - (col0 << FIXED_SHIFT); + let fx1 = p1x - (col1 << FIXED_SHIFT); + if col0 == col1 { + cell(row_idx, col0, fx0, p0y, fx1, p1y); + return; + } + let dx = p1x - p0x; + let dy = p1y - p0y; + let pp = (FIXED_ONE - fx0) * dy; + var cy = p0y + (pp / dx); + cell(row_idx, col0, fx0, p0y, FIXED_ONE, cy); + var idx = col0 + 1; + if idx != col1 { + var md = (pp % dx) - dx; + let p = FIXED_ONE * dy; + let lift = p / dx; + let rem = p % dx; + for (; idx != col1; idx++) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dx; + delta++; + } + let ny = cy + delta; + cell(row_idx, idx, 0, cy, FIXED_ONE, ny); + cy = ny; + } + } + cell(row_idx, col1, 0, cy, fx1, p1y); + } + + fn row_down_r_v(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + if p0x < p1x { + row_down_r(row_idx, p0x, p0y, p1x, p1y); + } else { + let ci = (p0x - find_adjustment(p0x)) >> FIXED_SHIFT; + let x = p0x - (ci << FIXED_SHIFT); + cell_vertical(ci, row_idx, x, p0y, p1y); + } + } + + fn row_up_r(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + let col0 = p0x >> FIXED_SHIFT; + let col1 = (p1x - 1) >> FIXED_SHIFT; + let fx0 = p0x - (col0 << FIXED_SHIFT); + let fx1 = p1x - (col1 << FIXED_SHIFT); + if col0 == col1 { + cell(row_idx, col0, fx0, p0y, fx1, p1y); + return; + } + let dx = p1x - p0x; + let dy = p0y - p1y; + let pp = (FIXED_ONE - fx0) * dy; + var cy = p0y - (pp / dx); + cell(row_idx, col0, fx0, p0y, FIXED_ONE, cy); + var idx = col0 + 1; + if idx != col1 { + var md = (pp % dx) - dx; + let p = FIXED_ONE * dy; + let lift = p / dx; + let rem = p % dx; + for (; idx != col1; idx++) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dx; + delta++; + } + let ny = cy - delta; + cell(row_idx, idx, 0, cy, FIXED_ONE, ny); + cy = ny; + } + } + cell(row_idx, col1, 0, cy, fx1, p1y); + } + + fn row_up_r_v(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + if p0x < p1x { + row_up_r(row_idx, p0x, p0y, p1x, p1y); + } else { + let ci = (p0x - find_adjustment(p0x)) >> FIXED_SHIFT; + let x = p0x - (ci << FIXED_SHIFT); + cell_vertical(ci, row_idx, x, p0y, p1y); + } + } + + fn row_down_l(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + let col0 = (p0x - 1) >> FIXED_SHIFT; + let col1 = p1x >> FIXED_SHIFT; + let fx0 = p0x - (col0 << FIXED_SHIFT); + let fx1 = p1x - (col1 << FIXED_SHIFT); + if col0 == col1 { + cell(row_idx, col0, fx0, p0y, fx1, p1y); + return; + } + let dx = p0x - p1x; + let dy = p1y - p0y; + let pp = fx0 * dy; + var cy = p0y + (pp / dx); + cell(row_idx, col0, fx0, p0y, 0, cy); + var idx = col0 - 1; + if idx != col1 { + var md = (pp % dx) - dx; + let p = FIXED_ONE * dy; + let lift = p / dx; + let rem = p % dx; + for (; idx != col1; idx--) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dx; + delta++; + } + let ny = cy + delta; + cell(row_idx, idx, FIXED_ONE, cy, 0, ny); + cy = ny; + } + } + cell(row_idx, col1, FIXED_ONE, cy, fx1, p1y); + } + + fn row_down_l_v(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + if p0x > p1x { + row_down_l(row_idx, p0x, p0y, p1x, p1y); + } else { + let ci = (p0x - find_adjustment(p0x)) >> FIXED_SHIFT; + let x = p0x - (ci << FIXED_SHIFT); + cell_vertical(ci, row_idx, x, p0y, p1y); + } + } + + fn row_up_l(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + let col0 = (p0x - 1) >> FIXED_SHIFT; + let col1 = p1x >> FIXED_SHIFT; + let fx0 = p0x - (col0 << FIXED_SHIFT); + let fx1 = p1x - (col1 << FIXED_SHIFT); + if col0 == col1 { + cell(row_idx, col0, fx0, p0y, fx1, p1y); + return; + } + let dx = p0x - p1x; + let dy = p0y - p1y; + let pp = fx0 * dy; + var cy = p0y - (pp / dx); + cell(row_idx, col0, fx0, p0y, 0, cy); + var idx = col0 - 1; + if idx != col1 { + var md = (pp % dx) - dx; + let p = FIXED_ONE * dy; + let lift = p / dx; + let rem = p % dx; + for (; idx != col1; idx--) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dx; + delta++; + } + let ny = cy - delta; + cell(row_idx, idx, FIXED_ONE, cy, 0, ny); + cy = ny; + } + } + cell(row_idx, col1, FIXED_ONE, cy, fx1, p1y); + } + + fn row_up_l_v(row_idx: i32, p0x: i32, p0y: i32, p1x: i32, p1y: i32) { + if p0x > p1x { + row_up_l(row_idx, p0x, p0y, p1x, p1y); + } else { + let ci = (p0x - find_adjustment(p0x)) >> FIXED_SHIFT; + let x = p0x - (ci << FIXED_SHIFT); + cell_vertical(ci, row_idx, x, p0y, p1y); + } + } + + fn line_down_r(row0: i32, row1: i32, x0: i32, y0: i32, x1: i32, y1: i32) { + let dx = x1 - x0; + let dy = y1 - y0; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let p_init = (FIXED_ONE - fy0) * dx; + let delta_init = p_init / dy; + var cx = x0 + delta_init; + row_down_r_v(row0, x0, fy0, cx, FIXED_ONE); + var row = row0 + 1; + if row != row1 { + var md = (p_init % dy) - dy; + let p = FIXED_ONE * dx; + let lift = p / dy; + let rem = p % dy; + for (; row != row1; row++) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dy; + delta++; + } + let nx = cx + delta; + row_down_r_v(row, cx, 0, nx, FIXED_ONE); + cx = nx; + } + } + row_down_r_v(row1, cx, 0, x1, fy1); + } + + fn line_up_r(row0: i32, row1: i32, x0: i32, y0: i32, x1: i32, y1: i32) { + let dx = x1 - x0; + let dy = y0 - y1; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let p_init = fy0 * dx; + let delta_init = p_init / dy; + var cx = x0 + delta_init; + row_up_r_v(row0, x0, fy0, cx, 0); + var row = row0 - 1; + if row != row1 { + var md = (p_init % dy) - dy; + let p = FIXED_ONE * dx; + let lift = p / dy; + let rem = p % dy; + for (; row != row1; row--) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dy; + delta++; + } + let nx = cx + delta; + row_up_r_v(row, cx, FIXED_ONE, nx, 0); + cx = nx; + } + } + row_up_r_v(row1, cx, FIXED_ONE, x1, fy1); + } + + fn line_down_l(row0: i32, row1: i32, x0: i32, y0: i32, x1: i32, y1: i32) { + let dx = x0 - x1; + let dy = y1 - y0; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let p_init = (FIXED_ONE - fy0) * dx; + let delta_init = p_init / dy; + var cx = x0 - delta_init; + row_down_l_v(row0, x0, fy0, cx, FIXED_ONE); + var row = row0 + 1; + if row != row1 { + var md = (p_init % dy) - dy; + let p = FIXED_ONE * dx; + let lift = p / dy; + let rem = p % dy; + for (; row != row1; row++) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dy; + delta++; + } + let nx = cx - delta; + row_down_l_v(row, cx, 0, nx, FIXED_ONE); + cx = nx; + } + } + row_down_l_v(row1, cx, 0, x1, fy1); + } + + fn line_up_l(row0: i32, row1: i32, x0: i32, y0: i32, x1: i32, y1: i32) { + let dx = x0 - x1; + let dy = y0 - y1; + let fy0 = y0 - (row0 << FIXED_SHIFT); + let fy1 = y1 - (row1 << FIXED_SHIFT); + let p_init = fy0 * dx; + let delta_init = p_init / dy; + var cx = x0 - delta_init; + row_up_l_v(row0, x0, fy0, cx, 0); + var row = row0 - 1; + if row != row1 { + var md = (p_init % dy) - dy; + let p = FIXED_ONE * dx; + let lift = p / dy; + let rem = p % dy; + for (; row != row1; row--) { + var delta = lift; + md += rem; + if md >= 0 { + md -= dy; + delta++; + } + let nx = cx - delta; + row_up_l_v(row, cx, FIXED_ONE, nx, 0); + cx = nx; + } + } + row_up_l_v(row1, cx, FIXED_ONE, x1, fy1); + } + + fn rasterize_line(x0: i32, y0: i32, x1: i32, y1: i32) { + if x0 == x1 { + let ci = (x0 - find_adjustment(x0)) >> FIXED_SHIFT; + if y0 < y1 { + vertical_down(ci, y0, y1, x0); + } else { + vertical_up(ci, y0, y1, x0); + } + return; + } + if y0 < y1 { + let r0 = y0 >> FIXED_SHIFT; + let r1 = (y1 - 1) >> FIXED_SHIFT; + if r0 == r1 { + let base_y = r0 << FIXED_SHIFT; + if x0 < x1 { + row_down_r(r0, x0, y0 - base_y, x1, y1 - base_y); + } else { + row_down_l(r0, x0, y0 - base_y, x1, y1 - base_y); + } + } else if x0 < x1 { + line_down_r(r0, r1, x0, y0, x1, y1); + } else { + line_down_l(r0, r1, x0, y0, x1, y1); + } return; } + let r0 = (y0 - 1) >> FIXED_SHIFT; + let r1 = y1 >> FIXED_SHIFT; + if r0 == r1 { + let base_y = r0 << FIXED_SHIFT; + if x0 < x1 { + row_up_r(r0, x0, y0 - base_y, x1, y1 - base_y); + } else { + row_up_l(r0, x0, y0 - base_y, x1, y1 - base_y); + } + } else if x0 < x1 { + line_up_r(r0, r1, x0, y0, x1, y1); + } else { + line_up_l(r0, r1, x0, y0, x1, y1); + } + } - let pixel_x = global_id.x; - let pixel_y = global_id.y % 16u; - if (pixel_x >= 16u || pixel_y >= 16u) { + fn clip_test(p: f32, q: f32, t0_in: f32, t1_in: f32) -> vec3 { + // Returns (t0, t1, valid) where valid > 0 means the segment is not rejected. + if p == 0.0 { + if q >= 0.0 { + return vec3(t0_in, t1_in, 1.0); + } + return vec3(t0_in, t1_in, -1.0); + } + let r = q / p; + if p < 0.0 { + if r > t1_in { + return vec3(t0_in, t1_in, -1.0); + } + return vec3(max(t0_in, r), t1_in, 1.0); + } + // p > 0 + if r < t0_in { + return vec3(t0_in, t1_in, -1.0); + } + return vec3(t0_in, min(t1_in, r), 1.0); + } + + struct ClippedEdge { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + valid: i32, + } + + fn clip_vertical(ex0: i32, ey0: i32, ex1: i32, ey1: i32, min_y: i32, max_y: i32) -> ClippedEdge { + var t0 = 0.0; + var t1 = 1.0; + let ox = f32(ex0); + let oy = f32(ey0); + let dx = f32(ex1 - ex0); + let dy = f32(ey1 - ey0); + let res1 = clip_test(-dy, oy - f32(min_y), t0, t1); + if res1.z < 0.0 { + return ClippedEdge(ex0, ey0, ex1, ey1, 0); + } + t0 = res1.x; + t1 = res1.y; + let res2 = clip_test(dy, f32(max_y) - oy, t0, t1); + if res2.z < 0.0 { + return ClippedEdge(ex0, ey0, ex1, ey1, 0); + } + t0 = res2.x; + t1 = res2.y; + var rx0 = ex0; + var ry0 = ey0; + var rx1 = ex1; + var ry1 = ey1; + if t1 < 1.0 { + rx1 = i32(round(ox + dx * t1)); + ry1 = i32(round(oy + dy * t1)); + } + if t0 > 0.0 { + rx0 = i32(round(ox + dx * t0)); + ry0 = i32(round(oy + dy * t0)); + } + if ry0 == ry1 { + return ClippedEdge(rx0, ry0, rx1, ry1, 0); + } + return ClippedEdge(rx0, ry0, rx1, ry1, 1); + } + + fn rasterize_edge(edge: Edge, band_top: i32, band_left_fixed: i32, clip_top_fixed: i32, clip_bottom_fixed: i32) { + let band_top_fixed = band_top << FIXED_SHIFT; + let ex0 = edge.x0 - band_left_fixed; + let ey0 = edge.y0; + let ex1 = edge.x1 - band_left_fixed; + let ey1 = edge.y1; + if ey0 >= clip_top_fixed && ey0 < clip_bottom_fixed && ey1 >= clip_top_fixed && ey1 < clip_bottom_fixed { + rasterize_line(ex0, ey0 - band_top_fixed, ex1, ey1 - band_top_fixed); return; } + let clipped = clip_vertical(ex0, ey0, ex1, ey1, clip_top_fixed, clip_bottom_fixed); + if clipped.valid == 0 { + return; + } + rasterize_line(clipped.x0, clipped.y0 - band_top_fixed, clipped.x1, clipped.y1 - band_top_fixed); + } - let dest_x = (tile_x * 16u) + pixel_x; - let dest_y = (tile_y * 16u) + pixel_y; + fn area_to_coverage(area_val: i32, fill_rule: u32) -> f32 { + let signed_area = area_val >> AREA_SHIFT; + var abs_area: i32; + if signed_area < 0 { + abs_area = -signed_area; + } else { + abs_area = signed_area; + } + var coverage: f32; + if fill_rule == 0u { + // Non-zero winding + if abs_area >= COV_STEPS { + coverage = 1.0; + } else { + coverage = f32(abs_area) * COV_SCALE; + } + } else { + // Even-odd + var wrapped = abs_area & EO_MASK; + if wrapped > COV_STEPS { + wrapped = EO_PERIOD - wrapped; + } + if wrapped >= COV_STEPS { + coverage = 1.0; + } else { + coverage = f32(wrapped) * COV_SCALE; + } + } + return coverage; + } + + // ----------------------------------------------------------------------- + // Main entry point + // ----------------------------------------------------------------------- - if (dest_x >= dispatch_config.target_width || dest_y >= dispatch_config.target_height) { + @compute @workgroup_size(16, 16, 1) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3 + ) { + let tile_x = wg_id.x; + let tile_y = wg_id.y; + let tile_index = tile_y * dispatch_config.tile_count_x + tile_x; + if tile_index >= dispatch_config.tile_count { return; } + let px = local_id.x; + let py = local_id.y; + let thread_id = py * 16u + px; + + let dest_x = tile_x * 16u + px; + let dest_y = tile_y * 16u + py; + + let in_bounds = dest_x < dispatch_config.target_width && dest_y < dispatch_config.target_height; + let source_x = i32(dest_x + dispatch_config.source_origin_x); let source_y = i32(dest_y + dispatch_config.source_origin_y); let output_x_i32 = i32(dest_x + dispatch_config.output_origin_x); let output_y_i32 = i32(dest_y + dispatch_config.output_origin_y); - let source = __LOAD_BACKDROP__; - var destination = vec4(source.rgb * source.a, source.a); + + var destination: vec4; + if in_bounds { + let source = __LOAD_BACKDROP__; + destination = vec4(source.rgb * source.a, source.a); + } + let dest_x_i32 = i32(dest_x); let dest_y_i32 = i32(dest_y); let tile_command_start = tile_starts[tile_index]; let tile_command_count = atomicLoad(&tile_counts[tile_index]); - var tile_command_offset: u32 = 0u; - loop { - if (tile_command_offset >= tile_command_count) { - break; - } - let command_index = tile_command_indices[tile_command_start + tile_command_offset]; + for (var tile_cmd_offset = 0u; tile_cmd_offset < tile_command_count; tile_cmd_offset++) { + let command_index = tile_command_indices[tile_command_start + tile_cmd_offset]; let command = commands[command_index]; - let command_min_x = bitcast(command.destination_x); - let command_min_y = bitcast(command.destination_y); - let command_max_x = command_min_x + i32(command.destination_width); - let command_max_y = command_min_y + i32(command.destination_height); - if (dest_x_i32 >= command_min_x && dest_x_i32 < command_max_x && dest_y_i32 >= command_min_y && dest_y_i32 < command_max_y) { - let local_x = dest_x_i32 - command_min_x; - let local_y = dest_y_i32 - command_min_y; - let coverage_x = bitcast(command.coverage_offset_x) + local_x; - let coverage_y = bitcast(command.coverage_offset_y) + local_y; - let coverage_value = textureLoad(coverage_texture, vec2(coverage_x, coverage_y), 0).x; - if (coverage_value > 0.0) { - let blend_percentage = u32_to_f32(command.blend_percentage); - let effective_coverage = coverage_value * blend_percentage; - - var brush = vec4( - u32_to_f32(command.solid_r), - u32_to_f32(command.solid_g), - u32_to_f32(command.solid_b), - u32_to_f32(command.solid_a)); - - if (command.brush_type == 1u) { - let origin_x = bitcast(command.brush_origin_x); - let origin_y = bitcast(command.brush_origin_y); - let region_x = i32(command.brush_region_x); - let region_y = i32(command.brush_region_y); - let region_w = i32(command.brush_region_width); - let region_h = i32(command.brush_region_height); - let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; - let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; - brush = __LOAD_BRUSH__; - } - let src = vec4(brush.rgb, brush.a * effective_coverage); - destination = compose_pixel(destination, src, command.color_blend_mode, command.alpha_composition_mode); + // Clear shared coverage memory. + atomicStore(&tile_cover[thread_id], 0); + atomicStore(&tile_area[thread_id], 0); + if px == 0u { + atomicStore(&tile_start_cover[py], 0); + } + workgroupBarrier(); + + // Determine this tile's position in coverage-local space. + let band_top = i32(tile_y * 16u) - command.edge_origin_y; + let band_bottom = band_top + 16; + let band_left_fixed = (i32(tile_x * 16u) - command.edge_origin_x) << FIXED_SHIFT; + + // CSR band lookup: which 16-row bands overlap this tile? + var first_band = band_top / 16; + if band_top < 0 && (band_top % 16) != 0 { + first_band -= 1; + } + first_band = max(first_band, 0); + var last_band = (band_bottom - 1) / 16; + if band_bottom - 1 < 0 && ((band_bottom - 1) % 16) != 0 { + last_band -= 1; + } + last_band = min(last_band, i32(command.csr_band_count) - 1); + + // Cooperatively rasterize edges from the relevant CSR bands. + let tile_top_fixed = band_top << FIXED_SHIFT; + let tile_bottom_fixed = tile_top_fixed + (i32(16) << FIXED_SHIFT); + for (var band = first_band; band <= last_band; band++) { + let csr_start = csr_offsets[command.csr_offsets_start + u32(band)]; + let csr_end = csr_offsets[command.csr_offsets_start + u32(band) + 1u]; + let band_edge_count = csr_end - csr_start; + // Clip to intersection of tile window and CSR band window + // to avoid double-counting edges that span multiple CSR bands. + let csr_band_top_fixed = (band * 16) << FIXED_SHIFT; + let csr_band_bottom_fixed = csr_band_top_fixed + (i32(16) << FIXED_SHIFT); + let clip_top = max(tile_top_fixed, csr_band_top_fixed); + let clip_bottom = min(tile_bottom_fixed, csr_band_bottom_fixed); + var ei = thread_id; + loop { + if ei >= band_edge_count { + break; + } + let edge_local_idx = csr_indices[csr_start + ei]; + let edge = edges[command.edge_start + edge_local_idx]; + rasterize_edge(edge, band_top, band_left_fixed, clip_top, clip_bottom); + ei += 256u; } } + workgroupBarrier(); - tile_command_offset += 1u; + // Compute coverage and compose for this pixel. + if in_bounds { + let cmd_min_x = bitcast(command.destination_x); + let cmd_min_y = bitcast(command.destination_y); + let cmd_max_x = cmd_min_x + i32(command.destination_width); + let cmd_max_y = cmd_min_y + i32(command.destination_height); + if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { + // Prefix sum of cover deltas for this row. + var cover = atomicLoad(&tile_start_cover[py]); + for (var col = 0u; col < px; col++) { + cover += atomicLoad(&tile_cover[py * 16u + col]); + } + let area_val = atomicLoad(&tile_area[py * 16u + px]) + (cover << AREA_SHIFT); + let coverage_value = area_to_coverage(area_val, command.fill_rule_value); + + if coverage_value > 0.0 { + let blend_percentage = u32_to_f32(command.blend_percentage); + let effective_coverage = coverage_value * blend_percentage; + + var brush = vec4( + u32_to_f32(command.solid_r), + u32_to_f32(command.solid_g), + u32_to_f32(command.solid_b), + u32_to_f32(command.solid_a)); + + if command.brush_type == 1u { + let origin_x = bitcast(command.brush_origin_x); + let origin_y = bitcast(command.brush_origin_y); + let region_x = i32(command.brush_region_x); + let region_y = i32(command.brush_region_y); + let region_w = i32(command.brush_region_width); + let region_h = i32(command.brush_region_height); + let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; + let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; + brush = __LOAD_BRUSH__; + } + + let src = vec4(brush.rgb, brush.a * effective_coverage); + destination = compose_pixel(destination, src, command.color_blend_mode, command.alpha_composition_mode); + } + } + } + workgroupBarrier(); } - let alpha = destination.a; - let rgb = unpremultiply(destination.rgb, alpha); - __STORE_OUTPUT__ + if in_bounds { + let alpha = destination.a; + let rgb = unpremultiply(destination.rgb, alpha); + __STORE_OUTPUT__ + } } """; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs new file mode 100644 index 000000000..8c06dfd0b --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Phase 2 of the parallel tile prefix sum: a single workgroup performs an +/// in-place exclusive prefix sum over the block_sums array from phase 1. +/// Supports up to 65536 blocks (256 * 256 = 16M tiles). +/// +internal static class PreparedCompositeTilePrefixBlockScanComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct PrefixConfig { + block_count: u32, + }; + + @group(0) @binding(0) var block_sums: array; + @group(0) @binding(1) var prefix_config: PrefixConfig; + + var shared_data: array; + + @compute @workgroup_size(256, 1, 1) + fn cs_main(@builtin(local_invocation_id) local_id: vec3) { + let tid = local_id.x; + let block_count = prefix_config.block_count; + + // Each thread processes multiple chunks of 256 blocks sequentially. + // This handles up to 65536 blocks (256 threads * 256 elements each). + var running_total = 0u; + var chunk_start = 0u; + loop { + if (chunk_start >= block_count) { + break; + } + + let global_index = chunk_start + tid; + var value = 0u; + if (global_index < block_count) { + value = block_sums[global_index]; + } + shared_data[tid] = value; + workgroupBarrier(); + + // Up-sweep. + for (var stride = 1u; stride < 256u; stride = stride * 2u) { + let index = (tid + 1u) * stride * 2u - 1u; + if (index < 256u) { + shared_data[index] = shared_data[index] + shared_data[index - stride]; + } + workgroupBarrier(); + } + + // Store chunk total and clear for down-sweep. + var chunk_total = 0u; + if (tid == 0u) { + chunk_total = shared_data[255]; + shared_data[255] = 0u; + } + workgroupBarrier(); + + // Down-sweep. + for (var stride = 128u; stride >= 1u; stride = stride / 2u) { + let index = (tid + 1u) * stride * 2u - 1u; + if (index < 256u) { + let temp = shared_data[index - stride]; + shared_data[index - stride] = shared_data[index]; + shared_data[index] = shared_data[index] + temp; + } + workgroupBarrier(); + } + + // Write back with running total offset. + if (global_index < block_count) { + block_sums[global_index] = shared_data[tid] + running_total; + } + workgroupBarrier(); + + // Broadcast chunk_total from thread 0 for next iteration. + if (tid == 0u) { + shared_data[0] = chunk_total; + } + workgroupBarrier(); + running_total = running_total + shared_data[0]; + workgroupBarrier(); + + chunk_start = chunk_start + 256u; + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs new file mode 100644 index 000000000..24af1f654 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Phase 1 of the parallel tile prefix sum: each workgroup computes a local +/// exclusive prefix sum over 256 tile counts, writes per-tile starts, and +/// stores the workgroup total into a block_sums buffer. +/// +internal static class PreparedCompositeTilePrefixLocalComputeShader +{ + /// + /// The number of tiles processed by each workgroup. + /// + public const int TilesPerWorkgroup = 256; + + private static readonly byte[] CodeBytes = + [ + .. """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, + }; + + @group(0) @binding(0) var tile_counts: array>; + @group(0) @binding(1) var tile_starts: array; + @group(0) @binding(2) var block_sums: array; + @group(0) @binding(3) var dispatch_config: DispatchConfig; + + var shared_data: array; + + @compute @workgroup_size(256, 1, 1) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3 + ) { + let tid = local_id.x; + let global_index = wg_id.x * 256u + tid; + + // Load tile count (0 if out of range). + var value = 0u; + if (global_index < dispatch_config.tile_count) { + value = atomicLoad(&tile_counts[global_index]); + } + shared_data[tid] = value; + workgroupBarrier(); + + // Up-sweep (reduce) phase. + for (var stride = 1u; stride < 256u; stride = stride * 2u) { + let index = (tid + 1u) * stride * 2u - 1u; + if (index < 256u) { + shared_data[index] = shared_data[index] + shared_data[index - stride]; + } + workgroupBarrier(); + } + + // Store total and clear last element for down-sweep. + if (tid == 0u) { + block_sums[wg_id.x] = shared_data[255]; + shared_data[255] = 0u; + } + workgroupBarrier(); + + // Down-sweep phase. + for (var stride = 128u; stride >= 1u; stride = stride / 2u) { + let index = (tid + 1u) * stride * 2u - 1u; + if (index < 256u) { + let temp = shared_data[index - stride]; + shared_data[index - stride] = shared_data[index]; + shared_data[index] = shared_data[index] + temp; + } + workgroupBarrier(); + } + + // Write exclusive prefix sum to output. + if (global_index < dispatch_config.tile_count) { + tile_starts[global_index] = shared_data[tid]; + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs new file mode 100644 index 000000000..81015edc1 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Phase 3 of the parallel tile prefix sum: each workgroup adds its +/// block prefix from block_sums to all tile_starts in its range. +/// Workgroup 0 is skipped (its prefix is 0). +/// +internal static class PreparedCompositeTilePrefixPropagateComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct DispatchConfig { + target_width: u32, + target_height: u32, + tile_count_x: u32, + tile_count_y: u32, + tile_count: u32, + command_count: u32, + source_origin_x: u32, + source_origin_y: u32, + output_origin_x: u32, + output_origin_y: u32, + width_in_bins: u32, + height_in_bins: u32, + bin_count: u32, + partition_count: u32, + binning_size: u32, + bin_data_start: u32, + }; + + @group(0) @binding(0) var block_sums: array; + @group(0) @binding(1) var tile_starts: array; + @group(0) @binding(2) var dispatch_config: DispatchConfig; + + @compute @workgroup_size(256, 1, 1) + fn cs_main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3 + ) { + if (wg_id.x == 0u) { + return; + } + + let global_index = wg_id.x * 256u + local_id.x; + if (global_index < dispatch_config.tile_count) { + tile_starts[global_index] = tile_starts[global_index] + block_sums[wg_id.x]; + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs deleted file mode 100644 index feca6417d..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/SegmentAllocComputeShader.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Allocates segment storage for coverage rasterization. -/// -internal static class SegmentAllocComputeShader -{ - /// - /// Gets the null-terminated WGSL source for segment allocation. - /// - private static readonly byte[] CodeBytes = - [ - .. - """ - // Segment allocation stage (simplified for single-path batches). - - const STAGE_BINNING: u32 = 0x1u; - const STAGE_TILE_ALLOC: u32 = 0x2u; - const STAGE_FLATTEN: u32 = 0x4u; - const STAGE_PATH_COUNT: u32 = 0x8u; - const STAGE_COARSE: u32 = 0x10u; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - ptcl: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend: atomic, - lines: atomic, - } - - struct Tile { - backdrop: i32, - segment_count_or_ix: u32, - } - - struct SegmentAllocParams { - tile_count: u32, - } - - @group(0) @binding(0) - var bump: BumpAllocators; - - @group(0) @binding(1) - var tiles: array; - - @group(0) @binding(2) - var tile_counts: array; - - @group(0) @binding(3) - var params: SegmentAllocParams; - - @compute @workgroup_size(256) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - let tile_ix = global_id.x; - if tile_ix >= params.tile_count { - return; - } - - let count = tiles[tile_ix].segment_count_or_ix; - tile_counts[tile_ix] = count; - if count == 0u { - return; - } - - let seg_offset = atomicAdd(&bump.segments, count); - tiles[tile_ix].segment_count_or_ix = ~seg_offset; - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 7bc45f908..5e18f7768 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Buffers.Binary; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; using SixLabors.ImageSharp.Memory; @@ -14,21 +13,15 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed unsafe partial class WebGPUDrawingBackend { - private const int TileWidth = 16; private const int TileHeight = 16; - private const float TileScale = 1F / TileWidth; - private const int LineStrideBytes = 24; - private const int PathStrideBytes = 32; - private const int TileStrideBytes = 8; - private const int SegmentCountStrideBytes = 8; - private const int SegmentStrideBytes = 24; - private const int SegmentAllocWorkgroupSize = 256; + private const int EdgeStrideBytes = 32; + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private const int CsrWorkgroupSize = 256; private readonly Dictionary coverageGeometryCache = []; private IMemoryOwner? cachedCoverageLineUpload; private int cachedCoverageLineLength; - private IMemoryOwner? cachedCoveragePathUpload; - private int cachedCoveragePathLength; /// /// Writes bind-group entries and returns the number of populated entries. @@ -41,45 +34,40 @@ internal sealed unsafe partial class WebGPUDrawingBackend private delegate void ComputePassDispatch(ComputePassEncoder* pass); /// - /// Builds and dispatches the full coverage rasterization pipeline for flattened paths. + /// Builds flattened fixed-point edge geometry for all coverage definitions and uploads to a GPU buffer. + /// Each edge is in 24.8 fixed-point format with min_row/max_row and CSR metadata. /// - /// The canvas pixel type. - /// The active flush context. - /// Coverage definitions participating in the current flush. - /// The current processing configuration. - /// Receives the output coverage texture view. - /// Receives per-definition atlas placement information. - /// Receives an error message when the operation fails. - /// when rasterization setup and dispatch succeed; otherwise . - private bool TryCreateCoverageTextureFromFlattened( + private bool TryCreateEdgeBuffer( WebGPUFlushContext flushContext, List definitions, Configuration configuration, - out TextureView* coverageView, - out CoveragePlacement[] coveragePlacements, + out WgpuBuffer* edgeBuffer, + out nuint edgeBufferSize, + out EdgePlacement[] edgePlacements, + out int totalEdgeCount, + out int totalCsrEntries, + out int totalCsrIndices, out string? error) where TPixel : unmanaged, IPixel { - coverageView = null; - coveragePlacements = []; + edgeBuffer = null; + edgeBufferSize = 0; + edgePlacements = []; + totalEdgeCount = 0; + totalCsrEntries = 0; + totalCsrIndices = 0; error = null; if (definitions.Count == 0) { return true; } - CoveragePathBuild[] pathBuilds = new CoveragePathBuild[definitions.Count]; - coveragePlacements = new CoveragePlacement[definitions.Count]; - int totalLineCount = 0; - int totalTileCount = 0; - ulong totalEstimatedSegments = 0; - int atlasWidthInTiles = 0; - int atlasHeightInTiles = 0; - int currentTileY = 0; - uint? fillRuleValue = null; - uint? aliasedValue = null; - - // First pass: validate inputs, resolve/build cached geometry, and pack atlas placements. + edgePlacements = new EdgePlacement[definitions.Count]; + int runningEdgeStart = 0; + int runningCsrOffset = 0; + + // First pass: resolve/build cached geometry and compute edge placements. + CachedCoverageGeometry?[] geometries = new CachedCoverageGeometry?[definitions.Count]; for (int i = 0; i < definitions.Count; i++) { CompositionCoverageDefinition definition = definitions[i]; @@ -91,62 +79,26 @@ private bool TryCreateCoverageTextureFromFlattened( } uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; - uint isAliased = definition.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased ? 1u : 0u; - if ((fillRuleValue.HasValue && fillRuleValue.Value != fillRule) || - (aliasedValue.HasValue && aliasedValue.Value != isAliased)) - { - error = "Mixed rasterization modes are not supported in one flush coverage pass."; - return false; - } - - fillRuleValue ??= fillRule; - aliasedValue ??= isAliased; - - int widthInTiles = (int)DivideRoundUp(interest.Width, TileWidth); - int heightInTiles = (int)DivideRoundUp(interest.Height, TileHeight); - int originTileX = 0; - int originTileY = currentTileY; - int originX = originTileX * TileWidth; - int originY = originTileY * TileHeight; + int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); CoverageDefinitionIdentity identity = new(definition); if (!this.coverageGeometryCache.TryGetValue(identity, out CachedCoverageGeometry? geometry)) { - IMemoryOwner? lineOwner = null; - try - { - if (!TryBuildLineBuffer( - definition.Path, - in interest, - definition.RasterizerOptions.SamplingOrigin, - configuration.MemoryAllocator, - out lineOwner, - out int lineCount, - out _, - out _, - out _, - out _, - out uint estimatedSegments, - out error)) - { - return false; - } - - geometry = new CachedCoverageGeometry( - lineOwner, - lineCount, - estimatedSegments, - widthInTiles, - heightInTiles, - interest.Width, - interest.Height); - lineOwner = null; - this.coverageGeometryCache[identity] = geometry; - } - finally + if (!TryBuildFixedPointEdges( + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + configuration.MemoryAllocator, + out IMemoryOwner? edgeOwner, + out int edgeCount, + out int bandOverlaps, + out error)) { - lineOwner?.Dispose(); + return false; } + + geometry = new CachedCoverageGeometry(edgeOwner, edgeCount, bandCount, bandOverlaps); + this.coverageGeometryCache[identity] = geometry; } if (geometry is null) @@ -155,78 +107,82 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - pathBuilds[i] = new CoveragePathBuild( - geometry, - originTileX, - originTileY, - originX, - originY); - coveragePlacements[i] = new CoveragePlacement(originX, originY, interest.Width, interest.Height); - - totalLineCount = checked(totalLineCount + geometry.LineCount); - totalEstimatedSegments += geometry.EstimatedSegments; - atlasWidthInTiles = Math.Max(atlasWidthInTiles, geometry.WidthInTiles); - atlasHeightInTiles = Math.Max(atlasHeightInTiles, originTileY + geometry.HeightInTiles); - currentTileY += geometry.HeightInTiles; - } + geometries[i] = geometry; - totalTileCount = checked(atlasWidthInTiles * atlasHeightInTiles); + // bandCount + 1 entries in CSR offsets (the +1 is the sentinel for the last band's end). + int csrEntriesForDef = bandCount + 1; + edgePlacements[i] = new EdgePlacement( + (uint)runningEdgeStart, + (uint)geometry.EdgeCount, + fillRule, + (uint)runningCsrOffset, + (uint)bandCount); - int atlasWidth = Math.Max(1, atlasWidthInTiles * TileWidth); - int atlasHeight = Math.Max(1, atlasHeightInTiles * TileHeight); - if (!TryCreateCoverageTexture( - flushContext, - atlasWidth, - atlasHeight, - configuration.MemoryAllocator, - totalLineCount == 0, - out Texture* coverageTexture, - out coverageView, - out error)) - { - return false; + runningEdgeStart += geometry.EdgeCount; + runningCsrOffset += csrEntriesForDef; + totalCsrIndices += geometry.TotalBandOverlaps; } - flushContext.TrackTexture(coverageTexture); - flushContext.TrackTextureView(coverageView); - if (totalLineCount == 0) + totalEdgeCount = runningEdgeStart; + totalCsrEntries = runningCsrOffset; + + if (totalEdgeCount == 0) { + // Provide a minimal buffer so the bind group is valid. + edgeBufferSize = EdgeStrideBytes; + if (!TryGetOrCreateCoverageBuffer( + flushContext, + "coverage-aggregated-edges", + BufferUsage.Storage | BufferUsage.CopyDst, + edgeBufferSize, + out edgeBuffer, + out error)) + { + return false; + } + return true; } - // Build a merged line buffer with coordinates translated into atlas space. - int lineBufferBytes = checked(totalLineCount * LineStrideBytes); - using IMemoryOwner lineUploadOwner = configuration.MemoryAllocator.Allocate(lineBufferBytes); - Span lineUpload = lineUploadOwner.Memory.Span[..lineBufferBytes]; - int mergedLineIndex = 0; - for (int pathIndex = 0; pathIndex < pathBuilds.Length; pathIndex++) + // Build merged edge buffer with CSR metadata. + int edgeBufferBytes = checked(totalEdgeCount * EdgeStrideBytes); + edgeBufferSize = (nuint)edgeBufferBytes; + using IMemoryOwner edgeUploadOwner = configuration.MemoryAllocator.Allocate(edgeBufferBytes); + Span edgeUpload = edgeUploadOwner.Memory.Span[..edgeBufferBytes]; + + int mergedEdgeIndex = 0; + for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { - CoveragePathBuild build = pathBuilds[pathIndex]; - CachedCoverageGeometry geometry = build.Geometry; - if (geometry.LineCount == 0 || geometry.LineOwner is null) + CachedCoverageGeometry? geometry = geometries[defIndex]; + if (geometry is null || geometry.EdgeCount == 0 || geometry.EdgeOwner is null) { continue; } - ReadOnlySpan sourceLines = geometry.LineOwner.Memory.Span[..(geometry.LineCount * LineStrideBytes)]; - for (int lineIndex = 0; lineIndex < geometry.LineCount; lineIndex++) + EdgePlacement placement = edgePlacements[defIndex]; + ReadOnlySpan sourceEdges = geometry.EdgeOwner.Memory.Span[..(geometry.EdgeCount * EdgeStrideBytes)]; + + for (int edgeIndex = 0; edgeIndex < geometry.EdgeCount; edgeIndex++) { - int sourceOffset = lineIndex * LineStrideBytes; - float x0 = ReadFloat(sourceLines, sourceOffset + 8) + build.OriginX; - float y0 = ReadFloat(sourceLines, sourceOffset + 12) + build.OriginY; - float x1 = ReadFloat(sourceLines, sourceOffset + 16) + build.OriginX; - float y1 = ReadFloat(sourceLines, sourceOffset + 20) + build.OriginY; - WriteLine(lineUpload, mergedLineIndex, (uint)pathIndex, x0, y0, x1, y1); - mergedLineIndex++; + int srcOffset = edgeIndex * EdgeStrideBytes; + int dstOffset = mergedEdgeIndex * EdgeStrideBytes; + + // Copy x0, y0, x1, y1, min_row, max_row (24 bytes). + sourceEdges.Slice(srcOffset, 24).CopyTo(edgeUpload.Slice(dstOffset, 24)); + + // Set csr_band_offset and definition_edge_start. + BinaryPrimitives.WriteUInt32LittleEndian(edgeUpload.Slice(dstOffset + 24, 4), placement.CsrOffsetsStart); + BinaryPrimitives.WriteUInt32LittleEndian(edgeUpload.Slice(dstOffset + 28, 4), placement.EdgeStart); + mergedEdgeIndex++; } } if (!TryGetOrCreateCoverageBuffer( flushContext, - "coverage-aggregated-lines", + "coverage-aggregated-edges", BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)lineBufferBytes, - out WgpuBuffer* lineBuffer, + (nuint)edgeBufferBytes, + out edgeBuffer, out error)) { return false; @@ -234,8 +190,8 @@ private bool TryCreateCoverageTextureFromFlattened( if (!this.TryUploadDirtyCoverageRange( flushContext, - lineBuffer, - lineUpload, + edgeBuffer, + edgeUpload, ref this.cachedCoverageLineUpload, ref this.cachedCoverageLineLength, out error)) @@ -243,249 +199,6 @@ private bool TryCreateCoverageTextureFromFlattened( return false; } - // Build per-path metadata that maps each path into its tile span inside the atlas. - int pathBufferBytes = checked(pathBuilds.Length * PathStrideBytes); - using IMemoryOwner pathUploadOwner = configuration.MemoryAllocator.Allocate(pathBufferBytes); - Span pathUpload = pathUploadOwner.Memory.Span[..pathBufferBytes]; - int tileBase = 0; - for (int i = 0; i < pathBuilds.Length; i++) - { - CoveragePathBuild build = pathBuilds[i]; - WritePath( - pathUpload.Slice(i * PathStrideBytes, PathStrideBytes), - (uint)build.OriginTileX, - (uint)build.OriginTileY, - (uint)(build.OriginTileX + atlasWidthInTiles), - (uint)(build.OriginTileY + build.Geometry.HeightInTiles), - (uint)tileBase); - tileBase = checked(tileBase + (atlasWidthInTiles * build.Geometry.HeightInTiles)); - } - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-paths", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)pathBufferBytes, - out WgpuBuffer* pathBuffer, - out error)) - { - return false; - } - - if (!this.TryUploadDirtyCoverageRange( - flushContext, - pathBuffer, - pathUpload, - ref this.cachedCoveragePathUpload, - ref this.cachedCoveragePathLength, - out error)) - { - return false; - } - - int tileBufferBytes = checked(totalTileCount * TileStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-tiles", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileBufferBytes, - out WgpuBuffer* tileBuffer, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileBuffer, - 0, - (nuint)tileBufferBytes); - - int tileCountsBytes = checked(totalTileCount * sizeof(uint)); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-tile-counts", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileCountsBytes, - out WgpuBuffer* tileCountsBuffer, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileCountsBuffer, - 0, - (nuint)tileCountsBytes); - - if (totalEstimatedSegments > int.MaxValue) - { - error = "Coverage segment estimate overflow."; - return false; - } - - uint segCountsCapacity = totalEstimatedSegments == 0 ? 1u : checked((uint)totalEstimatedSegments); - uint segmentsCapacity = segCountsCapacity; - int segCountsBytes = checked((int)segCountsCapacity * SegmentCountStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segment-counts", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)segCountsBytes, - out WgpuBuffer* segCountsBuffer, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - segCountsBuffer, - 0, - (nuint)segCountsBytes); - - int segmentsBytes = checked((int)segmentsCapacity * SegmentStrideBytes); - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segments", - BufferUsage.Storage, - (nuint)segmentsBytes, - out WgpuBuffer* segmentsBuffer, - out error)) - { - return false; - } - - RasterConfig config = new() - { - WidthInTiles = (uint)atlasWidthInTiles, - HeightInTiles = (uint)atlasHeightInTiles, - TargetWidth = (uint)atlasWidth, - TargetHeight = (uint)atlasHeight, - BaseColor = 0, - NDrawObj = 0, - NPath = (uint)pathBuilds.Length, - NClip = 0, - BinDataStart = 0, - PathtagBase = 0, - PathdataBase = 0, - DrawtagBase = 0, - DrawdataBase = 0, - TransformBase = 0, - StyleBase = 0, - LinesSize = (uint)totalLineCount, - BinningSize = (uint)pathBuilds.Length, - TilesSize = (uint)totalTileCount, - SegCountsSize = segCountsCapacity, - SegmentsSize = segmentsCapacity, - BlendSize = 1, - PtclSize = 1 - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-raster-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* configBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, (nuint)Unsafe.SizeOf()); - - BumpAllocatorsData bumpData = new() - { - Failed = 0, - Binning = 0, - Ptcl = 0, - Tile = 0, - SegCounts = 0, - Segments = 0, - Blend = 0, - Lines = (uint)totalLineCount - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-bump", - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* bumpBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, bumpBuffer, 0, &bumpData, (nuint)Unsafe.SizeOf()); - - IndirectCountData indirectData = default; - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-indirect", - BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* indirectBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, indirectBuffer, 0, &indirectData, (nuint)Unsafe.SizeOf()); - - SegmentAllocConfig segmentAllocConfig = new() { TileCount = (uint)totalTileCount }; - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-segment-alloc", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* segmentAllocBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, segmentAllocBuffer, 0, &segmentAllocConfig, (nuint)Unsafe.SizeOf()); - - CoverageConfig coverageConfig = new() - { - TargetWidth = (uint)atlasWidth, - TargetHeight = (uint)atlasHeight, - TileOriginX = 0, - TileOriginY = 0, - TileWidthInTiles = (uint)atlasWidthInTiles, - TileHeightInTiles = (uint)atlasHeightInTiles, - FillRule = fillRuleValue.GetValueOrDefault(0), - IsAliased = aliasedValue.GetValueOrDefault(0) - }; - - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-coverage-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* coverageConfigBuffer, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, coverageConfigBuffer, 0, &coverageConfig, (nuint)Unsafe.SizeOf()); - - // Dispatch compute stages in pipeline order: count -> backdrop -> alloc -> emit segments -> fine raster. - if (!this.DispatchPathCountSetup(flushContext, bumpBuffer, indirectBuffer, out error) || - !this.DispatchPathCount(flushContext, configBuffer, bumpBuffer, lineBuffer, pathBuffer, tileBuffer, segCountsBuffer, indirectBuffer, out error) || - !this.DispatchBackdrop(flushContext, configBuffer, tileBuffer, atlasHeightInTiles, out error) || - !this.DispatchSegmentAlloc(flushContext, bumpBuffer, tileBuffer, tileCountsBuffer, segmentAllocBuffer, totalTileCount, out error) || - !this.DispatchPathTilingSetup(flushContext, bumpBuffer, indirectBuffer, out error) || - !this.DispatchPathTiling(flushContext, bumpBuffer, segCountsBuffer, lineBuffer, pathBuffer, tileBuffer, segmentsBuffer, indirectBuffer, out error) || - !this.DispatchCoverageFine(flushContext, coverageConfigBuffer, tileBuffer, tileCountsBuffer, segmentsBuffer, coverageView, atlasWidthInTiles, atlasHeightInTiles, out error)) - { - return false; - } - error = null; return true; } @@ -540,7 +253,6 @@ private bool TryUploadDirtyCoverageRange( ReadOnlySpan sourceWords = MemoryMarshal.Cast(source[..commonAlignedLength]); ReadOnlySpan cachedWords = MemoryMarshal.Cast(cached[..commonAlignedLength]); - // Scan forward in 32-bit words first, then finish any remaining tail bytes. int firstDifferentWord = 0; while (firstDifferentWord < sourceWords.Length && cachedWords[firstDifferentWord] == sourceWords[firstDifferentWord]) { @@ -553,11 +265,8 @@ private bool TryUploadDirtyCoverageRange( firstDifferent++; } - // No upload needed when the source payload matches the cached upload exactly. if (firstDifferent < source.Length) { - // Trim unchanged suffix in reverse. Start with bytes above the aligned word boundary, - // then continue with 32-bit word comparisons. int lastDifferent = source.Length - 1; if (lastDifferent < commonLength) { @@ -577,27 +286,15 @@ private bool TryUploadDirtyCoverageRange( if (lastWordIndex >= firstWordIndex) { - // End on the containing word boundary; this may include up to 3 unchanged bytes. lastDifferent = Math.Min(lastDifferent, (lastWordIndex * sizeof(uint)) + (sizeof(uint) - 1)); } } - int uploadLength = (lastDifferent - firstDifferent) + 1; - - // Only write the dirty range to reduce queue upload bandwidth on repeated flushes. - // QueueWriteBuffer requires 4-byte aligned offsets and sizes. - // firstDifferent/uploadLength come from byte-wise diffing, so they can land - // in the middle of a 32-bit value. Expand the upload window to 4-byte bounds. - // `& ~0x3` clears the lower 2 bits (align down to previous multiple of 4). int uploadOffset = firstDifferent & ~0x3; - - int uploadEnd = firstDifferent + uploadLength; - - // `(x + 3) & ~0x3` rounds up to the next multiple of 4. - // Clamp afterwards so the rounded end never exceeds source length. + int uploadEnd = firstDifferent + (lastDifferent - firstDifferent) + 1; uploadEnd = (uploadEnd + 3) & ~0x3; uploadEnd = Math.Min(uploadEnd, source.Length); - uploadLength = uploadEnd - uploadOffset; + int uploadLength = uploadEnd - uploadOffset; fixed (byte* sourcePtr = source) { @@ -629,46 +326,38 @@ private void DisposeCoverageResources() this.cachedCoverageLineUpload?.Dispose(); this.cachedCoverageLineUpload = null; this.cachedCoverageLineLength = 0; - this.cachedCoveragePathUpload?.Dispose(); - this.cachedCoveragePathUpload = null; - this.cachedCoveragePathLength = 0; } /// - /// Flattens a path into the compact line format consumed by coverage compute shaders. + /// Flattens a path into fixed-point (24.8) edge format for GPU rasterization. + /// Each edge record is 32 bytes: x0, y0, x1, y1, min_row, max_row, 0, 0. /// - private static bool TryBuildLineBuffer( + private static bool TryBuildFixedPointEdges( IPath path, in Rectangle interest, RasterizerSamplingOrigin samplingOrigin, MemoryAllocator allocator, - out IMemoryOwner? lineOwner, - out int lineCount, - out float minX, - out float minY, - out float maxX, - out float maxY, - out uint estimatedSegments, + out IMemoryOwner? edgeOwner, + out int edgeCount, + out int totalBandOverlaps, out string? error) { error = null; - lineOwner = null; - lineCount = 0; - estimatedSegments = 0; - minX = float.PositiveInfinity; - minY = float.PositiveInfinity; - maxX = float.NegativeInfinity; - maxY = float.NegativeInfinity; + edgeOwner = null; + edgeCount = 0; + totalBandOverlaps = 0; bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + // First pass: count valid edges. List simplePaths = []; foreach (ISimplePath simplePath in path.Flatten()) { simplePaths.Add(simplePath); } + int maxEdgeCount = 0; for (int i = 0; i < simplePaths.Count; i++) { ReadOnlySpan points = simplePaths[i].Points.Span; @@ -677,57 +366,26 @@ private static bool TryBuildLineBuffer( continue; } - for (int j = 0; j < points.Length; j++) - { - float x = (points[j].X - interest.X) + samplingOffsetX; - float y = (points[j].Y - interest.Y) + samplingOffsetY; - if (x < minX) - { - minX = x; - } - - if (y < minY) - { - minY = y; - } - - if (x > maxX) - { - maxX = x; - } - - if (y > maxY) - { - maxY = y; - } - } - - int contourSegmentCount = simplePaths[i].IsClosed - ? points.Length - : points.Length - 1; - if (contourSegmentCount <= 0) + int segmentCount = simplePaths[i].IsClosed ? points.Length : points.Length - 1; + if (segmentCount > 0) { - continue; + maxEdgeCount += segmentCount; } - - lineCount += contourSegmentCount; } - if (lineCount == 0) + if (maxEdgeCount == 0) { - minX = 0; - minY = 0; - maxX = 0; - maxY = 0; return true; } - int lineBufferBytes = checked(lineCount * LineStrideBytes); - lineOwner = allocator.Allocate(lineBufferBytes); - Span lineBytes = lineOwner.Memory.Span[..lineBufferBytes]; - lineBytes.Clear(); + int height = interest.Height; + int bufferBytes = checked(maxEdgeCount * EdgeStrideBytes); + IMemoryOwner tempOwner = allocator.Allocate(bufferBytes); + Span edgeBytes = tempOwner.Memory.Span[..bufferBytes]; + edgeBytes.Clear(); - int lineIndex = 0; + int validEdgeCount = 0; + int bandOverlaps = 0; for (int i = 0; i < simplePaths.Count; i++) { ReadOnlySpan points = simplePaths[i].Points.Span; @@ -737,9 +395,7 @@ private static bool TryBuildLineBuffer( } bool contourClosed = simplePaths[i].IsClosed; - int segmentCount = contourClosed - ? points.Length - : points.Length - 1; + int segmentCount = contourClosed ? points.Length : points.Length - 1; if (segmentCount <= 0) { continue; @@ -755,616 +411,167 @@ private static bool TryBuildLineBuffer( } PointF p1 = points[nextIndex]; - float x0 = (p0.X - interest.X) + samplingOffsetX; - float y0 = (p0.Y - interest.Y) + samplingOffsetY; - float x1 = (p1.X - interest.X) + samplingOffsetX; - float y1 = (p1.Y - interest.Y) + samplingOffsetY; - WriteLine(lineBytes, lineIndex, x0, y0, x1, y1); - estimatedSegments += EstimateSegmentCount(x0, y0, x1, y1); - lineIndex++; - } - } - - return true; - } - - /// - /// Writes a single line record using the default path index. - /// - private static void WriteLine(Span destination, int lineIndex, float x0, float y0, float x1, float y1) - => WriteLine(destination, lineIndex, 0u, x0, y0, x1, y1); - - /// - /// Writes a single line record with an explicit path index. - /// - private static void WriteLine(Span destination, int lineIndex, uint pathIndex, float x0, float y0, float x1, float y1) - { - int offset = lineIndex * LineStrideBytes; - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), pathIndex); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset + 4, 4), 0u); - WriteFloat(destination, offset + 8, x0); - WriteFloat(destination, offset + 12, y0); - WriteFloat(destination, offset + 16, x1); - WriteFloat(destination, offset + 20, y1); - } + float fx0 = (p0.X - interest.X) + samplingOffsetX; + float fy0 = (p0.Y - interest.Y) + samplingOffsetY; + float fx1 = (p1.X - interest.X) + samplingOffsetX; + float fy1 = (p1.Y - interest.Y) + samplingOffsetY; + + // Convert to 24.8 fixed-point. + int x0 = (int)MathF.Round(fx0 * FixedOne); + int y0 = (int)MathF.Round(fy0 * FixedOne); + int x1 = (int)MathF.Round(fx1 * FixedOne); + int y1 = (int)MathF.Round(fy1 * FixedOne); + + // Skip horizontal edges (no coverage contribution). + if (y0 == y1) + { + continue; + } - /// - /// Writes a path bounding record using a default tile base. - /// - private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1) - => WritePath(destination, x0, y0, x1, y1, 0u); + // Compute min/max row (pixel coordinates), clamped to interest. + int yMinFixed = Math.Min(y0, y1); + int yMaxFixed = Math.Max(y0, y1); + int minRow = Math.Max(0, yMinFixed >> FixedShift); + int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); - /// - /// Writes a path bounding record with an explicit tile base offset. - /// - private static void WritePath(Span destination, uint x0, uint y0, uint x1, uint y1, uint tiles) - { - BinaryPrimitives.WriteUInt32LittleEndian(destination[..4], x0); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4, 4), y0); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8, 4), x1); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), y1); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16, 4), tiles); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(20, 4), 0u); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(24, 4), 0u); - BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(28, 4), 0u); - } + if (minRow > maxRow) + { + continue; + } - /// - /// Reads a 32-bit floating-point value from a little-endian byte span. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float ReadFloat(ReadOnlySpan source, int offset) - => BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(source.Slice(offset, 4))); + // Write edge record (32 bytes). + int offset = validEdgeCount * EdgeStrideBytes; + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset, 4), x0); + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 4, 4), y0); + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 8, 4), x1); + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 12, 4), y1); + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 16, 4), minRow); + BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 20, 4), maxRow); + int minBand = minRow / TileHeight; + int maxBand = maxRow / TileHeight; + bandOverlaps += maxBand - minBand + 1; + validEdgeCount++; + } + } - /// - /// Writes a 32-bit floating-point value to a little-endian byte span. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFloat(Span destination, int offset, float value) - => BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(offset, 4), (uint)BitConverter.SingleToInt32Bits(value)); + edgeCount = validEdgeCount; + totalBandOverlaps = bandOverlaps; - /// - /// Estimates how many tile segments a line contributes during path tiling. - /// - private static uint EstimateSegmentCount(float x0, float y0, float x1, float y1) - { - float s0x = x0 * TileScale; - float s0y = y0 * TileScale; - float s1x = x1 * TileScale; - float s1y = y1 * TileScale; - uint countX = SpanTiles(s0x, s1x); - uint countY = SpanTiles(s0y, s1y); - if (countX > 0) + if (validEdgeCount == 0) { - countX -= 1; + tempOwner.Dispose(); + return true; } - return countX + countY; + edgeOwner = tempOwner; + return true; } /// - /// Computes the number of tiles spanned by two coordinates along one axis. + /// Creates and executes a compute pass for a coverage pipeline stage. /// - private static uint SpanTiles(float a, float b) + private bool DispatchComputePass( + WebGPUFlushContext flushContext, + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + BindGroupEntryWriter entryWriter, + ComputePassDispatch dispatch, + out string? error) { - float max = MathF.Max(a, b); - float min = MathF.Min(a, b); - float span = MathF.Ceiling(max) - MathF.Floor(min); - if (span < 1F) + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) { - span = 1F; + error = $"Failed to begin compute pass for pipeline '{pipelineKey}'."; + return false; } - return (uint)span; + try + { + return this.DispatchIntoComputePass( + flushContext, + passEncoder, + pipelineKey, + shaderCode, + bindGroupLayoutFactory, + entryWriter, + dispatch, + out error); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } } - /// - /// Creates the coverage output texture and view used by the fine rasterization pass. - /// - private static bool TryCreateCoverageTexture( + private bool DispatchIntoComputePass( WebGPUFlushContext flushContext, - int width, - int height, - MemoryAllocator allocator, - bool clearOnCreate, - out Texture* coverageTexture, - out TextureView* coverageView, + ComputePassEncoder* passEncoder, + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + BindGroupEntryWriter entryWriter, + ComputePassDispatch dispatch, out string? error) { - TextureDescriptor descriptor = new() - { - Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = TextureFormat.R32float, - MipLevelCount = 1, - SampleCount = 1 - }; - - coverageTexture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); - if (coverageTexture is null) + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + pipelineKey, + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) { - coverageView = null; - error = "Failed to create coverage texture."; return false; } - TextureViewDescriptor viewDescriptor = new() + BindGroupEntry* entries = stackalloc BindGroupEntry[8]; + uint entryCount = entryWriter(new Span(entries, 8)); + + BindGroupDescriptor bindGroupDescriptor = new() { - Format = descriptor.Format, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All + Layout = bindGroupLayout, + EntryCount = entryCount, + Entries = entries }; - coverageView = flushContext.Api.TextureCreateView(coverageTexture, in viewDescriptor); - if (coverageView is null) + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) { - flushContext.Api.TextureRelease(coverageTexture); - error = "Failed to create coverage texture view."; + error = $"Failed to create bind group for pipeline '{pipelineKey}'."; return false; } - if (clearOnCreate) - { - int rowBytes = checked(width * sizeof(float)); - int byteCount = checked(rowBytes * height); - using IMemoryOwner zeroOwner = allocator.Allocate(byteCount); - Span zeroData = zeroOwner.Memory.Span[..byteCount]; - zeroData.Clear(); - ImageCopyTexture destination = new() - { - Texture = coverageTexture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All - }; - - Extent3D writeSize = new((uint)width, (uint)height, 1); - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = (uint)rowBytes, - RowsPerImage = (uint)height - }; - - fixed (byte* zeroPtr = zeroData) - { - flushContext.Api.QueueWriteTexture( - flushContext.Queue, - in destination, - zeroPtr, - (nuint)byteCount, - in layout, - in writeSize); - } - } + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + dispatch(passEncoder); error = null; return true; } /// - /// Dispatches the path-count setup shader that initializes indirect dispatch counts. + /// Creates the bind-group layout used by the CSR count shader. /// - private bool DispatchPathCountSetup( - WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* indirectBuffer, - out string? error) - => this.DispatchComputePass( - flushContext, - "path-count-setup", - PathCountSetupComputeShader.Code, - TryCreatePathCountSetupBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = indirectBuffer, Offset = 0, Size = nuint.MaxValue }; - return 2; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), - out error); - - /// - /// Dispatches the path-count shader that computes per-tile segment counts. - /// - private bool DispatchPathCount( - WebGPUFlushContext flushContext, - WgpuBuffer* configBuffer, - WgpuBuffer* bumpBuffer, - WgpuBuffer* lineBuffer, - WgpuBuffer* pathBuffer, - WgpuBuffer* tileBuffer, - WgpuBuffer* segCountsBuffer, - WgpuBuffer* indirectBuffer, - out string? error) - => this.DispatchComputePass( - flushContext, - "path-count", - PathCountComputeShader.Code, - TryCreatePathCountBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = configBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = lineBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = pathBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[5] = new BindGroupEntry { Binding = 5, Buffer = segCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - return 6; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, 0), - out error); - - /// - /// Dispatches the segment-allocation shader that computes per-tile segment offsets. - /// - private bool DispatchSegmentAlloc( - WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* tileBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* segmentAllocBuffer, - int tileCount, - out string? error) - => this.DispatchComputePass( - flushContext, - "segment-alloc", - SegmentAllocComputeShader.Code, - TryCreateSegmentAllocBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = segmentAllocBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 4; - }, - (pass) => - { - uint dispatchX = DivideRoundUp(tileCount, SegmentAllocWorkgroupSize); - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, dispatchX, 1, 1); - }, - out error); - - /// - /// Dispatches the backdrop prefix shader that accumulates backdrop values across tile rows. - /// - private bool DispatchBackdrop( - WebGPUFlushContext flushContext, - WgpuBuffer* configBuffer, - WgpuBuffer* tileBuffer, - int heightInTiles, - out string? error) - => this.DispatchComputePass( - flushContext, - "backdrop", - BackdropComputeShader.Code, - TryCreateBackdropBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = configBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; - return 2; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)heightInTiles, 1, 1), - out error); - - /// - /// Dispatches the path-tiling setup shader that prepares indirect counts for segment emission. - /// - private bool DispatchPathTilingSetup( - WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* indirectBuffer, - out string? error) - => this.DispatchComputePass( - flushContext, - "path-tiling-setup", - PathTilingSetupComputeShader.Code, - TryCreatePathTilingSetupBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = indirectBuffer, Offset = 0, Size = nuint.MaxValue }; - return 2; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), - out error); - - /// - /// Dispatches the path-tiling shader that emits clipped segments into per-tile storage. - /// - private bool DispatchPathTiling( - WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* segCountsBuffer, - WgpuBuffer* lineBuffer, - WgpuBuffer* pathBuffer, - WgpuBuffer* tileBuffer, - WgpuBuffer* segmentsBuffer, - WgpuBuffer* indirectBuffer, - out string? error) - => this.DispatchComputePass( - flushContext, - "path-tiling", - PathTilingComputeShader.Code, - TryCreatePathTilingBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = bumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = segCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = lineBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = pathBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[5] = new BindGroupEntry { Binding = 5, Buffer = segmentsBuffer, Offset = 0, Size = nuint.MaxValue }; - return 6; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(pass, indirectBuffer, 0), - out error); - - /// - /// Dispatches the fine coverage shader that rasterizes tile segments into the output texture. - /// - private bool DispatchCoverageFine( - WebGPUFlushContext flushContext, - WgpuBuffer* coverageConfigBuffer, - WgpuBuffer* tileBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* segmentsBuffer, - TextureView* coverageView, - int tileWidth, - int tileHeight, - out string? error) - => this.DispatchComputePass( - flushContext, - "coverage-fine", - CoverageFineComputeShader.Code, - TryCreateCoverageFineBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = coverageConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = segmentsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, TextureView = coverageView }; - return 5; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)tileWidth, (uint)tileHeight, 1), - out error); - - /// - /// Creates and executes a compute pass for a coverage pipeline stage. - /// - private bool DispatchComputePass( - WebGPUFlushContext flushContext, - string pipelineKey, - ReadOnlySpan shaderCode, - WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, - BindGroupEntryWriter entryWriter, - ComputePassDispatch dispatch, + private static bool TryCreateCsrCountBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, out string? error) { - if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - pipelineKey, - shaderCode, - bindGroupLayoutFactory, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, - out error)) - { - return false; - } - - BindGroupEntry* entries = stackalloc BindGroupEntry[8]; - uint entryCount = entryWriter(new Span(entries, 8)); - - BindGroupDescriptor bindGroupDescriptor = new() - { - Layout = bindGroupLayout, - EntryCount = entryCount, - Entries = entries - }; - - BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); - if (bindGroup is null) - { - error = $"Failed to create bind group for pipeline '{pipelineKey}'."; - return false; - } - - flushContext.TrackBindGroup(bindGroup); - ComputePassDescriptor passDescriptor = default; - ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); - if (passEncoder is null) - { - error = $"Failed to begin compute pass for pipeline '{pipelineKey}'."; - return false; - } - - try - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - dispatch(passEncoder); - } - finally - { - flushContext.Api.ComputePassEncoderEnd(passEncoder); - flushContext.Api.ComputePassEncoderRelease(passEncoder); - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the path-count setup shader. - /// - private static bool TryCreatePathCountSetupBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; entries[0] = new BindGroupLayoutEntry { Binding = 0, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 2, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create path count setup bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the path-count shader. - /// - private static bool TryCreatePathCountBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = LineStrideBytes - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = PathStrideBytes - } - }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = TileStrideBytes - } - }; - entries[5] = new BindGroupLayoutEntry - { - Binding = 5, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = SegmentCountStrideBytes - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 6, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create path count bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the segment-allocation shader. - /// - private static bool TryCreateSegmentAllocBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() + MinBindingSize = 0 } }; entries[1] = new BindGroupLayoutEntry @@ -1375,7 +582,7 @@ private static bool TryCreateSegmentAllocBindGroupLayout( { Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = TileStrideBytes + MinBindingSize = 0 } }; entries[2] = new BindGroupLayoutEntry @@ -1383,34 +590,23 @@ private static bool TryCreateSegmentAllocBindGroupLayout( Binding = 2, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = sizeof(uint) - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() + MinBindingSize = sizeof(uint) } }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, + EntryCount = 3, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create segment allocation bind group layout."; + error = "Failed to create CSR count bind group layout."; return false; } @@ -1419,126 +615,26 @@ private static bool TryCreateSegmentAllocBindGroupLayout( } /// - /// Creates the bind-group layout used by the backdrop prefix shader. + /// Creates the bind-group layout used by the CSR scatter shader. /// - private static bool TryCreateBackdropBindGroupLayout( + private static bool TryCreateCsrScatterBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; entries[0] = new BindGroupLayoutEntry { Binding = 0, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 2, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create backdrop bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the path-tiling setup shader. - /// - private static bool TryCreatePathTilingSetupBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 2, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create path tiling setup bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the path-tiling shader. - /// - private static bool TryCreatePathTilingBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; entries[1] = new BindGroupLayoutEntry { Binding = 1, @@ -1547,7 +643,7 @@ private static bool TryCreatePathTilingBindGroupLayout( { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, - MinBindingSize = SegmentCountStrideBytes + MinBindingSize = 0 } }; entries[2] = new BindGroupLayoutEntry @@ -1556,9 +652,9 @@ private static bool TryCreatePathTilingBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, + Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = LineStrideBytes + MinBindingSize = 0 } }; entries[3] = new BindGroupLayoutEntry @@ -1567,9 +663,9 @@ private static bool TryCreatePathTilingBindGroupLayout( Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, + Type = BufferBindingType.Storage, HasDynamicOffset = false, - MinBindingSize = PathStrideBytes + MinBindingSize = 0 } }; entries[4] = new BindGroupLayoutEntry @@ -1577,106 +673,12 @@ private static bool TryCreatePathTilingBindGroupLayout( Binding = 4, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = TileStrideBytes - } - }; - entries[5] = new BindGroupLayoutEntry - { - Binding = 5, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = SegmentStrideBytes - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 6, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create path tiling bind group layout."; - return false; - } - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by the fine coverage shader. - /// - private static bool TryCreateCoverageFineBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout { Type = BufferBindingType.Uniform, HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = TileStrideBytes - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, MinBindingSize = sizeof(uint) } }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = SegmentStrideBytes - } - }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - StorageTexture = new StorageTextureBindingLayout - { - Access = StorageTextureAccess.WriteOnly, - Format = TextureFormat.R32float, - ViewDimension = TextureViewDimension.Dimension2D - } - }; BindGroupLayoutDescriptor descriptor = new() { @@ -1687,7 +689,7 @@ private static bool TryCreateCoverageFineBindGroupLayout( layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create coverage fine bind group layout."; + error = "Failed to create CSR scatter bind group layout."; return false; } @@ -1696,138 +698,15 @@ private static bool TryCreateCoverageFineBindGroupLayout( } /// - /// Flattened path payload used during coverage rasterization. - /// - private readonly struct CoveragePathBuild - { - /// - /// Initializes a new instance of the struct. - /// - public CoveragePathBuild( - CachedCoverageGeometry geometry, - int originTileX, - int originTileY, - int originX, - int originY) - { - this.Geometry = geometry; - this.OriginTileX = originTileX; - this.OriginTileY = originTileY; - this.OriginX = originX; - this.OriginY = originY; - } - - /// - /// Gets the cached geometry payload. - /// - public CachedCoverageGeometry Geometry { get; } - - /// - /// Gets the atlas origin in tile coordinates on the X axis. - /// - public int OriginTileX { get; } - - /// - /// Gets the atlas origin in tile coordinates on the Y axis. - /// - public int OriginTileY { get; } - - /// - /// Gets the atlas origin in pixel coordinates on the X axis. - /// - public int OriginX { get; } - - /// - /// Gets the atlas origin in pixel coordinates on the Y axis. - /// - public int OriginY { get; } - } - - /// - /// Rasterizer dispatch configuration for a coverage pass. - /// - [StructLayout(LayoutKind.Sequential)] - private struct RasterConfig - { - public uint WidthInTiles; - public uint HeightInTiles; - public uint TargetWidth; - public uint TargetHeight; - public uint BaseColor; - public uint NDrawObj; - public uint NPath; - public uint NClip; - public uint BinDataStart; - public uint PathtagBase; - public uint PathdataBase; - public uint DrawtagBase; - public uint DrawdataBase; - public uint TransformBase; - public uint StyleBase; - public uint LinesSize; - public uint BinningSize; - public uint TilesSize; - public uint SegCountsSize; - public uint SegmentsSize; - public uint BlendSize; - public uint PtclSize; - public uint Pad0; - public uint Pad1; - } - - /// - /// GPU bump allocator counters for transient coverage buffers. - /// - [StructLayout(LayoutKind.Sequential)] - private struct BumpAllocatorsData - { - public uint Failed; - public uint Binning; - public uint Ptcl; - public uint Tile; - public uint SegCounts; - public uint Segments; - public uint Blend; - public uint Lines; - } - - /// - /// Indirect dispatch counts emitted by the coverage setup stage. + /// CSR configuration uniform passed to count and scatter shaders. /// [StructLayout(LayoutKind.Sequential)] - private struct IndirectCountData + private readonly struct CsrConfig { - public uint CountX; - public uint CountY; - public uint CountZ; - } - - /// - /// Segment allocator configuration for coverage path allocation. - /// - [StructLayout(LayoutKind.Sequential)] - private struct SegmentAllocConfig - { - public uint TileCount; - public uint Pad0; - public uint Pad1; - public uint Pad2; - } + public readonly uint TotalEdgeCount; - /// - /// Coverage pass configuration shared across compute stages. - /// - [StructLayout(LayoutKind.Sequential)] - private struct CoverageConfig - { - public uint TargetWidth; - public uint TargetHeight; - public uint TileOriginX; - public uint TileOriginY; - public uint TileWidthInTiles; - public uint TileHeightInTiles; - public uint FillRule; - public uint IsAliased; + public CsrConfig(uint totalEdgeCount) + => this.TotalEdgeCount = totalEdgeCount; } /// @@ -1835,64 +714,40 @@ private struct CoverageConfig /// private sealed class CachedCoverageGeometry : IDisposable { - /// - /// Initializes a new instance of the class. - /// public CachedCoverageGeometry( - IMemoryOwner? lineOwner, - int lineCount, - uint estimatedSegments, - int widthInTiles, - int heightInTiles, - int coverageWidth, - int coverageHeight) + IMemoryOwner? edgeOwner, + int edgeCount, + int bandCount, + int totalBandOverlaps) { - this.LineOwner = lineOwner; - this.LineCount = lineCount; - this.EstimatedSegments = estimatedSegments; - this.WidthInTiles = widthInTiles; - this.HeightInTiles = heightInTiles; - this.CoverageWidth = coverageWidth; - this.CoverageHeight = coverageHeight; + this.EdgeOwner = edgeOwner; + this.EdgeCount = edgeCount; + this.BandCount = bandCount; + this.TotalBandOverlaps = totalBandOverlaps; } /// - /// Gets the owned line segment buffer for the cached coverage geometry. - /// - public IMemoryOwner? LineOwner { get; } - - /// - /// Gets the number of lines stored in . - /// - public int LineCount { get; } - - /// - /// Gets the estimated number of segments generated for this geometry. - /// - public uint EstimatedSegments { get; } - - /// - /// Gets the coverage width in tiles. + /// Gets the owned fixed-point edge buffer. /// - public int WidthInTiles { get; } + public IMemoryOwner? EdgeOwner { get; } /// - /// Gets the coverage height in tiles. + /// Gets the number of edges stored in . /// - public int HeightInTiles { get; } + public int EdgeCount { get; } /// - /// Gets the coverage texture width in pixels. + /// Gets the number of 16-row CSR bands for this geometry. /// - public int CoverageWidth { get; } + public int BandCount { get; } /// - /// Gets the coverage texture height in pixels. + /// Gets the total number of edge-band overlaps (for CSR indices sizing). /// - public int CoverageHeight { get; } + public int TotalBandOverlaps { get; } /// public void Dispose() - => this.LineOwner?.Dispose(); + => this.EdgeOwner?.Dispose(); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 22572a57c..a08324302 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -38,7 +38,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { - private const int CompositeComputeWorkgroupSize = 8; private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; private const int CompositeBinTileCountX = 16; @@ -492,12 +491,16 @@ private bool TryRenderPreparedFlush( return true; } - if (!this.TryCreateCoverageTextureFromFlattened( + if (!this.TryCreateEdgeBuffer( flushContext, coverageDefinitions, configuration, - out TextureView* coverageView, - out CoveragePlacement[] coveragePlacements, + out WgpuBuffer* edgeBuffer, + out nuint edgeBufferSize, + out EdgePlacement[] edgePlacements, + out int totalEdgeCount, + out int totalCsrEntries, + out int totalCsrIndices, out error)) { return false; @@ -516,8 +519,12 @@ private bool TryRenderPreparedFlush( preparedBatches, batchCoverageIndices, commandCount, - coveragePlacements, - coverageView, + edgePlacements, + edgeBuffer, + edgeBufferSize, + totalEdgeCount, + totalCsrEntries, + totalCsrIndices, out error)) { return false; @@ -561,8 +568,12 @@ private bool TryDispatchPreparedCompositeCommands( List preparedBatches, int[] batchCoverageIndices, int commandCount, - CoveragePlacement[] coveragePlacements, - TextureView* coverageTextureView, + EdgePlacement[] edgePlacements, + WgpuBuffer* edgeBuffer, + nuint edgeBufferSize, + int totalEdgeCount, + int totalCsrEntries, + int totalCsrIndices, out string? error) where TPixel : unmanaged, IPixel { @@ -700,12 +711,18 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out return false; } - CoveragePlacement coveragePlacement = coveragePlacements[coverageDefinitionIndex]; + EdgePlacement edgePlacement = edgePlacements[coverageDefinitionIndex]; Rectangle destinationRegion = command.DestinationRegion; Point sourceOffset = command.SourceOffset; int destinationX = destinationRegion.X - targetLocalBounds.X; int destinationY = destinationRegion.Y - targetLocalBounds.Y; + + // Edge origin: transforms target-local pixel to edge-local space. + // edge_local = pixel - edge_origin, where edge_origin = destination - sourceOffset. + int edgeOriginX = destinationX - sourceOffset.X; + int edgeOriginY = destinationY - sourceOffset.Y; + int minTileX = destinationX / CompositeTileWidth; int minTileY = destinationY / CompositeTileHeight; int maxTileX = (destinationX + destinationRegion.Width - 1) / CompositeTileWidth; @@ -724,9 +741,12 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out destinationY, destinationRegion.Width, destinationRegion.Height, - sourceOffset.X + coveragePlacement.OriginX, - sourceOffset.Y + coveragePlacement.OriginY, - targetLocalBounds.Width, + edgePlacement.EdgeStart, + edgePlacement.FillRule, + edgeOriginX, + edgeOriginY, + edgePlacement.CsrOffsetsStart, + edgePlacement.CsrBandCount, brushType, brushOriginX, brushOriginY, @@ -923,54 +943,184 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out &dispatchConfig, dispatchConfigSize); - if (tileCommandCapacity > 0 && flushCommandCount > 0) + // Build CSR (Compressed Sparse Row) edge bucketing on GPU. + int csrEntries = Math.Max(totalCsrEntries, 1); + int csrIndices = Math.Max(totalCsrIndices, 1); + nuint csrBandCountsByteCount = checked((nuint)(csrEntries * sizeof(uint))); + nuint csrOffsetsByteCount = csrBandCountsByteCount; + nuint csrWriteCursorsByteCount = csrBandCountsByteCount; + nuint csrIndicesByteCount = checked((nuint)(csrIndices * sizeof(uint))); + + if (!TryGetOrCreateCoverageBuffer(flushContext, "csr-band-counts", BufferUsage.Storage | BufferUsage.CopyDst, csrBandCountsByteCount, out WgpuBuffer* csrBandCountsBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsByteCount, out WgpuBuffer* csrOffsetsBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-write-cursors", BufferUsage.Storage | BufferUsage.CopyDst, csrWriteCursorsByteCount, out WgpuBuffer* csrWriteCursorsBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesByteCount, out WgpuBuffer* csrIndicesBuffer, out error)) { - if (!this.DispatchPreparedCompositeBinning( - flushContext, - commandBboxesBuffer, - binHeaderBuffer, - binDataBuffer, - binningBumpBuffer, - dispatchConfigBuffer, - flushCommandCount, + return false; + } + + if (totalEdgeCount > 0 && totalCsrEntries > 0) + { + // CSR config uniform. + nuint csrConfigSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "csr-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + csrConfigSize, + out WgpuBuffer* csrConfigBuffer, + out _, out error)) { return false; } - if (!this.DispatchPreparedCompositeTileCount( + CsrConfig csrConfig = new((uint)totalEdgeCount); + flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrConfigBuffer, 0, &csrConfig, csrConfigSize); + + // Clear band counts before the count dispatch. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); + + // Dispatch CSR count: each thread processes one edge, atomicAdd to band_counts. + uint csrDispatchCount = DivideRoundUp(totalEdgeCount, CsrWorkgroupSize); + if (!this.DispatchComputePass( flushContext, - commandBboxesBuffer, - binHeaderBuffer, - binDataBuffer, - tileCountsBuffer, - dispatchConfigBuffer, - widthInBins, - heightInBins, + "csr-count", + CsrCountComputeShader.Code, + TryCreateCsrCountBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrBandCountsBuffer, Offset = 0, Size = csrBandCountsByteCount }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; + return 3; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, csrDispatchCount, 1, 1), out error)) { return false; } + // CSR prefix sum: exclusive prefix sum over band_counts → csr_offsets. + // Reuse the existing tile prefix infrastructure by providing a dispatch config + // with tile_count = totalCsrEntries. + nuint csrPrefixConfigSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "csr-prefix-dispatch-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + csrPrefixConfigSize, + out WgpuBuffer* csrPrefixDispatchConfigBuffer, + out _, + out error)) + { + return false; + } + + PreparedCompositeDispatchConfig csrPrefixConfig = new(0, 0, 0, 0, (uint)totalCsrEntries, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrPrefixDispatchConfigBuffer, 0, &csrPrefixConfig, csrPrefixConfigSize); + + // Dispatch tile prefix sum using band_counts as input and csr_offsets as output. if (!this.DispatchPreparedCompositeTilePrefix( flushContext, - tileCountsBuffer, - tileStartsBuffer, - dispatchConfigBuffer, + csrBandCountsBuffer, + csrOffsetsBuffer, + csrPrefixDispatchConfigBuffer, + totalCsrEntries, + out error)) + { + return false; + } + + // Clear write cursors before scatter. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrWriteCursorsBuffer, 0, csrWriteCursorsByteCount); + + // Dispatch CSR scatter: each thread processes one edge, scatters into csr_indices. + if (!this.DispatchComputePass( + flushContext, + "csr-scatter", + CsrScatterComputeShader.Code, + TryCreateCsrScatterBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = csrOffsetsByteCount }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrWriteCursorsBuffer, Offset = 0, Size = csrWriteCursorsByteCount }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrIndicesBuffer, Offset = 0, Size = csrIndicesByteCount }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; + return 5; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, csrDispatchCount, 1, 1), out error)) { return false; } + } + else + { + // No edges: clear CSR buffers. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsByteCount); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesByteCount); + } + + if (tileCommandCapacity > 0 && flushCommandCount > 0) + { + // Pass 1: Binning + tile count share a compute pass. + { + ComputePassDescriptor setupPassDescriptor = default; + ComputePassEncoder* pass = flushContext.Api.CommandEncoderBeginComputePass( + flushContext.CommandEncoder, in setupPassDescriptor); + if (pass is null) + { + error = "Failed to begin binning/tile-count compute pass."; + return false; + } + + try + { + if (!this.DispatchPreparedCompositeBinningInto( + flushContext, + pass, + commandBboxesBuffer, + binHeaderBuffer, + binDataBuffer, + binningBumpBuffer, + dispatchConfigBuffer, + flushCommandCount, + out error) || + !this.DispatchPreparedCompositeTileCountInto( + flushContext, + pass, + commandBboxesBuffer, + binHeaderBuffer, + binDataBuffer, + tileCountsBuffer, + dispatchConfigBuffer, + widthInBins, + heightInBins, + out error)) + { + return false; + } + } + finally + { + flushContext.Api.ComputePassEncoderEnd(pass); + flushContext.Api.ComputePassEncoderRelease(pass); + } + } - if (!this.DispatchPreparedCompositeTileFill( + // Tile prefix sum needs a buffer clear between passes, then + // prefix phases + tile fill share a second compute pass. + if (!this.DispatchPreparedCompositeTilePrefixAndTileFill( flushContext, commandBboxesBuffer, binHeaderBuffer, binDataBuffer, - tileStartsBuffer, tileCountsBuffer, + tileStartsBuffer, tileCommandIndicesBuffer, dispatchConfigBuffer, + tileCount, widthInBins, heightInBins, out error)) @@ -979,11 +1129,14 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out } } - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[9]; + // Fine composite dispatch with CSR buffers. + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[11]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, - TextureView = coverageTextureView + Buffer = edgeBuffer, + Offset = 0, + Size = edgeBufferSize }; bindGroupEntries[1] = new BindGroupEntry { @@ -1035,11 +1188,25 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out Offset = 0, Size = dispatchConfigSize }; + bindGroupEntries[9] = new BindGroupEntry + { + Binding = 9, + Buffer = csrOffsetsBuffer, + Offset = 0, + Size = csrOffsetsByteCount + }; + bindGroupEntries[10] = new BindGroupEntry + { + Binding = 10, + Buffer = csrIndicesBuffer, + Offset = 0, + Size = csrIndicesByteCount + }; BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 9, + EntryCount = 11, Entries = bindGroupEntries }; @@ -1065,9 +1232,9 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); flushContext.Api.ComputePassEncoderDispatchWorkgroups( passEncoder, - DivideRoundUp(CompositeTileWidth, CompositeComputeWorkgroupSize), - DivideRoundUp(CompositeTileHeight, CompositeComputeWorkgroupSize) * (uint)tileCountY, - (uint)tileCountX); + (uint)tileCountX, + (uint)tileCountY, + 1); } finally { @@ -1158,21 +1325,113 @@ private bool DispatchPreparedCompositeTilePrefix( WgpuBuffer* tileCountsBuffer, WgpuBuffer* tileStartsBuffer, WgpuBuffer* dispatchConfigBuffer, + int tileCount, out string? error) - => this.DispatchComputePass( - flushContext, - "prepared-composite-tile-prefix", - PreparedCompositeTilePrefixComputeShader.Code, - TryCreatePreparedCompositeTilePrefixBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 3; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), - out error); + { + const int tilesPerWorkgroup = PreparedCompositeTilePrefixLocalComputeShader.TilesPerWorkgroup; + int blockCount = checked((int)DivideRoundUp(tileCount, tilesPerWorkgroup)); + + // Allocate block_sums buffer for inter-workgroup prefix propagation. + nuint blockSumsSize = checked((nuint)(Math.Max(blockCount, 1) * sizeof(uint))); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "prepared-composite-tile-prefix-block-sums", + BufferUsage.Storage | BufferUsage.CopyDst, + blockSumsSize, + out WgpuBuffer* blockSumsBuffer, + out _, + out error)) + { + return false; + } + + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + blockSumsBuffer, + 0, + blockSumsSize); + + // Phase 1: Local prefix sum per workgroup + store block totals. + if (!this.DispatchComputePass( + flushContext, + "prepared-composite-tile-prefix-local", + PreparedCompositeTilePrefixLocalComputeShader.Code, + TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 4; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)blockCount, 1, 1), + out error)) + { + return false; + } + + if (blockCount <= 1) + { + // Single workgroup — no cross-block propagation needed. + return true; + } + + // Phase 2: Prefix sum over block_sums (single workgroup handles all blocks). + uint blockCountValue = (uint)blockCount; + nuint prefixConfigSize = sizeof(uint); + + // We need a small uniform buffer for the block count. + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "prepared-composite-tile-prefix-block-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + prefixConfigSize, + out WgpuBuffer* prefixConfigBuffer, + out _, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, prefixConfigBuffer, 0, &blockCountValue, prefixConfigSize); + + if (!this.DispatchComputePass( + flushContext, + "prepared-composite-tile-prefix-block-scan", + PreparedCompositeTilePrefixBlockScanComputeShader.Code, + TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = prefixConfigBuffer, Offset = 0, Size = prefixConfigSize }; + return 2; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), + out error)) + { + return false; + } + + // Phase 3: Propagate block prefixes to tile_starts. + if (!this.DispatchComputePass( + flushContext, + "prepared-composite-tile-prefix-propagate", + PreparedCompositeTilePrefixPropagateComputeShader.Code, + TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 3; + }, + (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)blockCount, 1, 1), + out error)) + { + return false; + } + + return true; + } private bool DispatchPreparedCompositeTileFill( WebGPUFlushContext flushContext, @@ -1213,6 +1472,251 @@ private bool DispatchPreparedCompositeTileFill( }, out error); + private bool DispatchPreparedCompositeBinningInto( + WebGPUFlushContext flushContext, + ComputePassEncoder* passEncoder, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, + WgpuBuffer* binningBumpBuffer, + WgpuBuffer* dispatchConfigBuffer, + int commandCount, + out string? error) + => this.DispatchIntoComputePass( + flushContext, + passEncoder, + "prepared-composite-binning", + PreparedCompositeBinningComputeShader.Code, + TryCreatePreparedCompositeBinningBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = binningBumpBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 5; + }, + (pass) => + { + uint workgroupCount = DivideRoundUp(commandCount, CompositeBinningWorkgroupSize); + if (workgroupCount > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCount, 1, 1); + } + }, + out error); + + private bool DispatchPreparedCompositeTileCountInto( + WebGPUFlushContext flushContext, + ComputePassEncoder* passEncoder, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* dispatchConfigBuffer, + int widthInBins, + int heightInBins, + out string? error) + => this.DispatchIntoComputePass( + flushContext, + passEncoder, + "prepared-composite-tile-count", + PreparedCompositeTileCountComputeShader.Code, + TryCreatePreparedCompositeTileCountBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 5; + }, + (pass) => + { + uint workgroupCountX = (uint)widthInBins; + uint workgroupCountY = (uint)heightInBins; + if (workgroupCountX > 0 && workgroupCountY > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); + } + }, + out error); + + private bool DispatchPreparedCompositeTilePrefixAndTileFill( + WebGPUFlushContext flushContext, + WgpuBuffer* commandBboxesBuffer, + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* binDataBuffer, + WgpuBuffer* tileCountsBuffer, + WgpuBuffer* tileStartsBuffer, + WgpuBuffer* tileCommandIndicesBuffer, + WgpuBuffer* dispatchConfigBuffer, + int tileCount, + int widthInBins, + int heightInBins, + out string? error) + { + const int tilesPerWorkgroup = PreparedCompositeTilePrefixLocalComputeShader.TilesPerWorkgroup; + int blockCount = checked((int)DivideRoundUp(tileCount, tilesPerWorkgroup)); + + // Allocate block_sums buffer for inter-workgroup prefix propagation. + nuint blockSumsSize = checked((nuint)(Math.Max(blockCount, 1) * sizeof(uint))); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "prepared-composite-tile-prefix-block-sums", + BufferUsage.Storage | BufferUsage.CopyDst, + blockSumsSize, + out WgpuBuffer* blockSumsBuffer, + out _, + out error)) + { + return false; + } + + // ClearBuffer must happen outside a compute pass. + flushContext.Api.CommandEncoderClearBuffer( + flushContext.CommandEncoder, + blockSumsBuffer, + 0, + blockSumsSize); + + // Prepare prefix config buffer (needed for multi-block case). + WgpuBuffer* prefixConfigBuffer = null; + nuint prefixConfigSize = sizeof(uint); + if (blockCount > 1) + { + uint blockCountValue = (uint)blockCount; + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "prepared-composite-tile-prefix-block-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + prefixConfigSize, + out prefixConfigBuffer, + out _, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, prefixConfigBuffer, 0, &blockCountValue, prefixConfigSize); + } + + // All prefix phases + tile fill share a single compute pass. + ComputePassDescriptor prefixPassDescriptor = default; + ComputePassEncoder* pass = flushContext.Api.CommandEncoderBeginComputePass( + flushContext.CommandEncoder, in prefixPassDescriptor); + if (pass is null) + { + error = "Failed to begin prefix/tile-fill compute pass."; + return false; + } + + try + { + // Phase 1: Local prefix sum per workgroup + store block totals. + if (!this.DispatchIntoComputePass( + flushContext, + pass, + "prepared-composite-tile-prefix-local", + PreparedCompositeTilePrefixLocalComputeShader.Code, + TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 4; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)blockCount, 1, 1), + out error)) + { + return false; + } + + if (blockCount > 1) + { + // Phase 2: Prefix sum over block_sums. + if (!this.DispatchIntoComputePass( + flushContext, + pass, + "prepared-composite-tile-prefix-block-scan", + PreparedCompositeTilePrefixBlockScanComputeShader.Code, + TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = prefixConfigBuffer, Offset = 0, Size = prefixConfigSize }; + return 2; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, 1, 1, 1), + out error)) + { + return false; + } + + // Phase 3: Propagate block prefixes to tile_starts. + if (!this.DispatchIntoComputePass( + flushContext, + pass, + "prepared-composite-tile-prefix-propagate", + PreparedCompositeTilePrefixPropagateComputeShader.Code, + TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 3; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)blockCount, 1, 1), + out error)) + { + return false; + } + } + + // Tile fill dispatched into the same pass. + if (!this.DispatchIntoComputePass( + flushContext, + pass, + "prepared-composite-tile-fill", + PreparedCompositeTileFillComputeShader.Code, + TryCreatePreparedCompositeTileFillBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[5] = new BindGroupEntry { Binding = 5, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[6] = new BindGroupEntry { Binding = 6, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; + return 7; + }, + (p) => + { + uint workgroupCountX = (uint)widthInBins; + uint workgroupCountY = (uint)heightInBins; + if (workgroupCountX > 0 && workgroupCountY > 0) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, workgroupCountX, workgroupCountY, 1); + } + }, + out error)) + { + return false; + } + + return true; + } + finally + { + flushContext.Api.ComputePassEncoderEnd(pass); + flushContext.Api.ComputePassEncoderRelease(pass); + } + } + private static bool TryGetOrCreateImageTextureView( WebGPUFlushContext flushContext, Image image, @@ -1291,16 +1795,16 @@ private static bool TryCreatePreparedCompositeFineBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[11]; entries[0] = new BindGroupLayoutEntry { Binding = 0, Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout + Buffer = new BufferBindingLayout { - SampleType = TextureSampleType.UnfilterableFloat, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 } }; entries[1] = new BindGroupLayoutEntry @@ -1391,10 +1895,32 @@ private static bool TryCreatePreparedCompositeFineBindGroupLayout( MinBindingSize = (nuint)Unsafe.SizeOf() } }; + entries[9] = new BindGroupLayoutEntry + { + Binding = 9, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[10] = new BindGroupLayoutEntry + { + Binding = 10, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 9, + EntryCount = 11, Entries = entries }; @@ -1569,7 +2095,123 @@ private static bool TryCreatePreparedCompositeBinningBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( + private static bool TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-prefix-local bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = sizeof(uint) + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create prepared composite tile-prefix-block-scan bind group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, @@ -1619,7 +2261,7 @@ private static bool TryCreatePreparedCompositeTilePrefixBindGroupLayout( layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite tile-prefix bind group layout."; + error = "Failed to create prepared composite tile-prefix-propagate bind group layout."; return false; } @@ -2150,23 +2792,26 @@ public override int GetHashCode() (int)this.samplingOrigin); } - private readonly struct CoveragePlacement + private readonly struct EdgePlacement { - public CoveragePlacement(int originX, int originY, int width, int height) + public EdgePlacement(uint edgeStart, uint edgeCount, uint fillRule, uint csrOffsetsStart, uint csrBandCount) { - this.OriginX = originX; - this.OriginY = originY; - this.Width = width; - this.Height = height; + this.EdgeStart = edgeStart; + this.EdgeCount = edgeCount; + this.FillRule = fillRule; + this.CsrOffsetsStart = csrOffsetsStart; + this.CsrBandCount = csrBandCount; } - public int OriginX { get; } + public uint EdgeStart { get; } + + public uint EdgeCount { get; } - public int OriginY { get; } + public uint FillRule { get; } - public int Width { get; } + public uint CsrOffsetsStart { get; } - public int Height { get; } + public uint CsrBandCount { get; } } /// @@ -2283,6 +2928,7 @@ public PreparedCompositeBinningBump(uint failed, uint binning) /// /// Prepared composite command parameters consumed by . + /// Layout matches the WGSL Params struct exactly (24 u32 fields = 96 bytes). /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeParameters @@ -2291,9 +2937,12 @@ private readonly struct PreparedCompositeParameters public readonly uint DestinationY; public readonly uint DestinationWidth; public readonly uint DestinationHeight; - public readonly uint CoverageOffsetX; - public readonly uint CoverageOffsetY; - public readonly uint TargetWidth; + public readonly uint EdgeStart; + public readonly uint FillRuleValue; + public readonly uint EdgeOriginX; + public readonly uint EdgeOriginY; + public readonly uint CsrOffsetsStart; + public readonly uint CsrBandCount; public readonly uint BrushType; public readonly uint BrushOriginX; public readonly uint BrushOriginY; @@ -2314,9 +2963,12 @@ public PreparedCompositeParameters( int destinationY, int destinationWidth, int destinationHeight, - int coverageOffsetX, - int coverageOffsetY, - int targetWidth, + uint edgeStart, + uint fillRuleValue, + int edgeOriginX, + int edgeOriginY, + uint csrOffsetsStart, + uint csrBandCount, uint brushType, int brushOriginX, int brushOriginY, @@ -2333,9 +2985,12 @@ public PreparedCompositeParameters( this.DestinationY = (uint)destinationY; this.DestinationWidth = (uint)destinationWidth; this.DestinationHeight = (uint)destinationHeight; - this.CoverageOffsetX = (uint)coverageOffsetX; - this.CoverageOffsetY = (uint)coverageOffsetY; - this.TargetWidth = (uint)targetWidth; + this.EdgeStart = edgeStart; + this.FillRuleValue = fillRuleValue; + this.EdgeOriginX = unchecked((uint)edgeOriginX); + this.EdgeOriginY = unchecked((uint)edgeOriginY); + this.CsrOffsetsStart = csrOffsetsStart; + this.CsrBandCount = csrBandCount; this.BrushType = brushType; this.BrushOriginX = (uint)brushOriginX; this.BrushOriginY = (uint)brushOriginY; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 954124622..f6a3468a2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -941,7 +941,7 @@ internal static void UploadTextureFromRegion( { int sourceStrideBytes = checked(sourceRegion.Buffer.RowStride * pixelSizeInBytes); long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + rowBytes; - long packedByteCountEstimate = (long)alignedRowBytes * sourceRegion.Height; + long packedByteCountEstimate = alignedRowBytes * sourceRegion.Height; // Only use the direct path when the stride satisfies WebGPU's alignment requirement. if ((uint)sourceStrideBytes == alignedRowBytes && directByteCount <= packedByteCountEstimate * 2) @@ -976,7 +976,7 @@ internal static void UploadTextureFromRegion( for (int y = 0; y < sourceRegion.Height; y++) { ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); - MemoryMarshal.AsBytes(sourceRow).Slice(0, rowBytes).CopyTo(packedData.Slice(y * alignedRowBytesInt, rowBytes)); + MemoryMarshal.AsBytes(sourceRow)[..rowBytes].CopyTo(packedData.Slice(y * alignedRowBytesInt, rowBytes)); } TextureDataLayout packedLayout = new() diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 7a099bd68..cb62938ec 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -395,11 +395,11 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.2F); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); Rectangle textRegion = Rectangle.Intersect( new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); - AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.03F); + AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.01F); } [Theory] @@ -527,7 +527,7 @@ void DrawAction(DrawingCanvas canvas) AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); } [Theory] @@ -905,6 +905,33 @@ private static void DebugSaveBackendTriplet( appendSourceFileOrDescription: false); } + private static void DebugSaveBackendTripletNoRef( + TestImageProvider provider, + string testName, + Image defaultImage, + Image cpuRegionImage, + Image nativeSurfaceImage) + where TPixel : unmanaged, IPixel + { + defaultImage.DebugSave( + provider, + $"{testName}_Default", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + cpuRegionImage.DebugSave( + provider, + $"{testName}_WebGPU_CPURegion", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + nativeSurfaceImage.DebugSave( + provider, + $"{testName}_WebGPU_NativeSurface", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + private static void AssertBackendTripletSimilarity( Image defaultImage, Image cpuRegionImage, @@ -971,6 +998,326 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) backend.TestingFallbackCompositeCoverageCallCount); } + [Theory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32)] + public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + PathBuilder pb = new(); + pb.AddLine(new PointF(30, 50), new PointF(370, 250)); + pb.AddLine(new PointF(370, 250), new PointF(200, 20)); + pb.CloseFigure(); + IPath path = pb.Build(); + Pen pen = Pens.Solid(Color.DarkBlue, 4F); + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, "DrawPath_Stroke", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + } + + [Theory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_MultipleSeparatePaths_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) + { + for (int i = 0; i < 20; i++) + { + float x = 20 + (i * 24); + float y = 20 + (i * 22); + canvas.Fill(new RectangularPolygon(x, y, 80, 60), brush); + } + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, "FillPath_MultipleSeparate", defaultImage, cpuRegionImage, nativeSurfaceImage); + + Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount >= 20); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true }, + ShapeOptions = new ShapeOptions + { + IntersectionRule = IntersectionRule.EvenOdd + } + }; + + PathBuilder pathBuilder = new(); + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(16, 16), + new PointF(240, 16), + new PointF(240, 240), + new PointF(16, 240) + ]); + pathBuilder.CloseFigure(); + + // Inner contour with same winding — EvenOdd should create a hole. + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(80, 80), + new PointF(176, 80), + new PointF(176, 176), + new PointF(80, 176) + ]); + pathBuilder.CloseFigure(); + + IPath path = pathBuilder.Build(); + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(path, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, "FillPath_EvenOdd", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + + // EvenOdd with same winding inner contour should create a hole at center. + Assert.Equal(defaultImage[128, 128], cpuRegionImage[128, 128]); + Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + } + + [Theory] + [WithSolidFilledImages(800, 600, "White", PixelTypes.Rgba32)] + public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Large polygon spanning most of the image to exercise many tiles. + Brush brush = Brushes.Solid(Color.Black); + EllipsePolygon ellipse = new(new PointF(400, 300), new SizeF(700, 500)); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, "FillPath_LargeTileCount", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + } + + [Theory] + [WithSolidFilledImages(300, 200, "White", PixelTypes.Rgba32)] + public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Brush redBrush = Brushes.Solid(Color.Red); + Brush blueBrush = Brushes.Solid(Color.Blue); + RectangularPolygon rect1 = new(20, 20, 120, 80); + RectangularPolygon rect2 = new(160, 100, 120, 80); + + // Default backend: two separate flushes + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas canvas1 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) + { + canvas1.Fill(rect1, redBrush); + canvas1.Flush(); + } + + using (DrawingCanvas canvas2 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) + { + canvas2.Fill(rect2, blueBrush); + canvas2.Flush(); + } + + // WebGPU backend: two separate flushes reusing the same backend + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + Configuration cpuConfig = Configuration.Default.Clone(); + cpuConfig.SetDrawingBackend(cpuRegionBackend); + + using (DrawingCanvas canvas1 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) + { + canvas1.Fill(rect1, redBrush); + canvas1.Flush(); + } + + using (DrawingCanvas canvas2 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) + { + canvas2.Fill(rect2, blueBrush); + canvas2.Flush(); + } + + // Native surface: two separate flushes reusing same backend + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + nativeSurfaceBackend, + defaultImage.Width, + defaultImage.Height, + isSrgb: false, + isPremultipliedAlpha: false, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration nativeConfig = Configuration.Default.Clone(); + nativeConfig.SetDrawingBackend(nativeSurfaceBackend); + Rectangle targetBounds = defaultImage.Bounds; + + // Upload initial white content + using Image initialImage = provider.GetImage(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryWriteTexture( + nativeSurfaceBackend, + textureHandle, + defaultImage.Width, + defaultImage.Height, + initialImage, + out string uploadError), + uploadError); + + using (DrawingCanvas canvas1 = + new(nativeConfig, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) + { + canvas1.Fill(rect1, redBrush); + canvas1.Flush(); + } + + using (DrawingCanvas canvas2 = + new(nativeConfig, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) + { + canvas2.Fill(rect2, blueBrush); + canvas2.Flush(); + } + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + nativeSurfaceBackend, + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image nativeSurfaceImage, + out string readError), + readError); + + using (nativeSurfaceImage) + { + DebugSaveBackendTripletNoRef(provider, "MultipleFlushes", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + } + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + private static Buffer2DRegion GetFrameRegion(Image image) where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png new file mode 100644 index 000000000..da297a340 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb52782b011b71d76299c1befc26eef0a0736606dd302dbeb787638d070dd76 +size 2288 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png new file mode 100644 index 000000000..58ed7c23a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800ce6ca4f9e7be5417df968ae74e094e7bb2a0823d033d6cfc0e733b8edb848 +size 2288 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png new file mode 100644 index 000000000..58ed7c23a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800ce6ca4f9e7be5417df968ae74e094e7bb2a0823d033d6cfc0e733b8edb848 +size 2288 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png index 09a15b690..8f178fd54 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0604ce8a71f1cf5be81da96ab3c8073e8bd15e2f5f18097ae63828f3e1a0d72 -size 4771 +oid sha256:d4777dbbd1b362e681a77bad5463048d3891d1dafe2f7c0849772f45653c3a1c +size 10941 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png index 09a15b690..8f178fd54 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0604ce8a71f1cf5be81da96ab3c8073e8bd15e2f5f18097ae63828f3e1a0d72 -size 4771 +oid sha256:d4777dbbd1b362e681a77bad5463048d3891d1dafe2f7c0849772f45653c3a1c +size 10941 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png index 70a207546..7024ebfef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:075b21e5fc234edb1fd161e069a34787b1dcdb3f29606e8f0cb0951968fdef49 -size 4825 +oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 +size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png index 70a207546..7024ebfef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:075b21e5fc234edb1fd161e069a34787b1dcdb3f29606e8f0cb0951968fdef49 -size 4825 +oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 +size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png index 7c6d73b75..ce3ccd62f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10da9c5b194281e56877383455f117043cd072fc57e247af7dfa6b42d968d422 -size 36884 +oid sha256:010f089d7793900d2afc224596bca74a90557b16aaefcc3f4453e51d277eb8db +size 36436 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png index 7c6d73b75..ce3ccd62f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10da9c5b194281e56877383455f117043cd072fc57e247af7dfa6b42d968d422 -size 36884 +oid sha256:010f089d7793900d2afc224596bca74a90557b16aaefcc3f4453e51d277eb8db +size 36436 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png new file mode 100644 index 000000000..09adf1c65 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 +size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png new file mode 100644 index 000000000..09adf1c65 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 +size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png new file mode 100644 index 000000000..09adf1c65 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 +size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png new file mode 100644 index 000000000..b37aefc21 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7921accb83e6cc2c3b537a0c354a403d15cfcad95aa1e352d5811a2082a69fc3 +size 6093 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png new file mode 100644 index 000000000..b37aefc21 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7921accb83e6cc2c3b537a0c354a403d15cfcad95aa1e352d5811a2082a69fc3 +size 6093 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b37aefc21 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7921accb83e6cc2c3b537a0c354a403d15cfcad95aa1e352d5811a2082a69fc3 +size 6093 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png new file mode 100644 index 000000000..3b25b6ccb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d +size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png new file mode 100644 index 000000000..1af25e3e1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f41a149e527d72a3b2b94948b625a15f9b73f5794a636225d4b5354f4b305bb4 +size 6938 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1af25e3e1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f41a149e527d72a3b2b94948b625a15f9b73f5794a636225d4b5354f4b305bb4 +size 6938 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png index 1eeb01770..fdb8fff15 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3bfb3cb3510c77beb21625d4d45bb3c10629f5469b4b4910d202e71967dce94 +oid sha256:d065e13c7ffde92b22a15a626842a92f39b65abcbf3a4c3561cd7341801f906c size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png index 1eeb01770..fdb8fff15 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3bfb3cb3510c77beb21625d4d45bb3c10629f5469b4b4910d202e71967dce94 +oid sha256:d065e13c7ffde92b22a15a626842a92f39b65abcbf3a4c3561cd7341801f906c size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png index 883df5636..5fc00164e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 -size 714 +oid sha256:5c5631a6d7a0f4278500f78a5e9e8ce25992b35b0f2fae5d00eab8a21ed6f95f +size 3682 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png index 883df5636..5fc00164e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 -size 714 +oid sha256:5c5631a6d7a0f4278500f78a5e9e8ce25992b35b0f2fae5d00eab8a21ed6f95f +size 3682 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png new file mode 100644 index 000000000..ced935c37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 +size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png new file mode 100644 index 000000000..ced935c37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 +size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ced935c37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 +size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png index 367e87dd7..218e72a90 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73308cc124098be2c2c84ff4b56009b7031533d543bf9ccb3094349737761fac -size 12907 +oid sha256:bef8c9b3fc1f857240c0d7a219e09604b14a14bed0c5344eae1a0670f651f1fd +size 12935 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png index 367e87dd7..218e72a90 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73308cc124098be2c2c84ff4b56009b7031533d543bf9ccb3094349737761fac -size 12907 +oid sha256:bef8c9b3fc1f857240c0d7a219e09604b14a14bed0c5344eae1a0670f651f1fd +size 12935 From ccf8a4a4a02a57ccda67ca5ad7c31bcbdc1bebbd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 12:39:00 +1000 Subject: [PATCH 088/136] Switch to CSR-based composite shaders --- ...uteShader.cs => CompositeComputeShader.cs} | 70 +- ....cs => CsrPrefixBlockScanComputeShader.cs} | 6 +- ...ader.cs => CsrPrefixLocalComputeShader.cs} | 6 +- ....cs => CsrPrefixPropagateComputeShader.cs} | 6 +- .../PreparedCompositeBinningComputeShader.cs | 176 --- ...PreparedCompositeTileCountComputeShader.cs | 115 -- .../PreparedCompositeTileFillComputeShader.cs | 119 -- ...reparedCompositeTilePrefixComputeShader.cs | 64 - .../WebGPUDrawingBackend.cs | 1393 +++-------------- 9 files changed, 272 insertions(+), 1683 deletions(-) rename src/ImageSharp.Drawing.WebGPU/Shaders/{PreparedCompositeFineComputeShader.cs => CompositeComputeShader.cs} (95%) rename src/ImageSharp.Drawing.WebGPU/Shaders/{PreparedCompositeTilePrefixBlockScanComputeShader.cs => CsrPrefixBlockScanComputeShader.cs} (94%) rename src/ImageSharp.Drawing.WebGPU/Shaders/{PreparedCompositeTilePrefixLocalComputeShader.cs => CsrPrefixLocalComputeShader.cs} (93%) rename src/ImageSharp.Drawing.WebGPU/Shaders/{PreparedCompositeTilePrefixPropagateComputeShader.cs => CsrPrefixPropagateComputeShader.cs} (89%) delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs delete mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs similarity index 95% rename from src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index ce8b5c92d..045abef14 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeFineComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// , operating per-tile with workgroup shared memory. /// Shader source is generated per texture format to match sampling/output requirements. /// -internal static class PreparedCompositeFineComputeShader +internal static class CompositeComputeShader { private static readonly object CacheSync = new(); private static readonly Dictionary ShaderCache = []; @@ -81,12 +81,9 @@ struct DispatchConfig { @group(0) @binding(2) var brush_texture: texture_2d<__BACKDROP_TEXEL_TYPE__>; @group(0) @binding(3) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; @group(0) @binding(4) var commands: array; - @group(0) @binding(5) var tile_starts: array; - @group(0) @binding(6) var tile_counts: array>; - @group(0) @binding(7) var tile_command_indices: array; - @group(0) @binding(8) var dispatch_config: DispatchConfig; - @group(0) @binding(9) var csr_offsets: array; - @group(0) @binding(10) var csr_indices: array; + @group(0) @binding(5) var dispatch_config: DispatchConfig; + @group(0) @binding(6) var csr_offsets: array; + @group(0) @binding(7) var csr_indices: array; // Workgroup shared memory for per-tile coverage accumulation. // Layout: 16 rows x 16 columns. Index = row * 16 + col. @@ -798,26 +795,27 @@ fn cs_main( let dest_x_i32 = i32(dest_x); let dest_y_i32 = i32(dest_y); + let tile_min_x = i32(tile_x * 16u); + let tile_min_y = i32(tile_y * 16u); + let tile_max_x = tile_min_x + 16; + let tile_max_y = tile_min_y + 16; - let tile_command_start = tile_starts[tile_index]; - let tile_command_count = atomicLoad(&tile_counts[tile_index]); - - for (var tile_cmd_offset = 0u; tile_cmd_offset < tile_command_count; tile_cmd_offset++) { - let command_index = tile_command_indices[tile_command_start + tile_cmd_offset]; + for (var command_index = 0u; command_index < dispatch_config.command_count; command_index++) { let command = commands[command_index]; - // Clear shared coverage memory. - atomicStore(&tile_cover[thread_id], 0); - atomicStore(&tile_area[thread_id], 0); - if px == 0u { - atomicStore(&tile_start_cover[py], 0); + // Tile vs command bounding box check (uniform across workgroup). + let cmd_min_x = bitcast(command.destination_x); + let cmd_min_y = bitcast(command.destination_y); + let cmd_max_x = cmd_min_x + i32(command.destination_width); + let cmd_max_y = cmd_min_y + i32(command.destination_height); + if tile_max_x <= cmd_min_x || tile_min_x >= cmd_max_x || tile_max_y <= cmd_min_y || tile_min_y >= cmd_max_y { + continue; } - workgroupBarrier(); // Determine this tile's position in coverage-local space. - let band_top = i32(tile_y * 16u) - command.edge_origin_y; + let band_top = tile_min_y - command.edge_origin_y; let band_bottom = band_top + 16; - let band_left_fixed = (i32(tile_x * 16u) - command.edge_origin_x) << FIXED_SHIFT; + let band_left_fixed = (tile_min_x - command.edge_origin_x) << FIXED_SHIFT; // CSR band lookup: which 16-row bands overlap this tile? var first_band = band_top / 16; @@ -831,6 +829,31 @@ fn cs_main( } last_band = min(last_band, i32(command.csr_band_count) - 1); + // Early exit: skip if no CSR bands have edges for this tile (uniform). + if first_band > last_band { + continue; + } + var tile_has_edges = false; + for (var b = first_band; b <= last_band; b++) { + let s = csr_offsets[command.csr_offsets_start + u32(b)]; + let e = csr_offsets[command.csr_offsets_start + u32(b) + 1u]; + if e > s { + tile_has_edges = true; + break; + } + } + if !tile_has_edges { + continue; + } + + // Clear shared coverage memory. + atomicStore(&tile_cover[thread_id], 0); + atomicStore(&tile_area[thread_id], 0); + if px == 0u { + atomicStore(&tile_start_cover[py], 0); + } + workgroupBarrier(); + // Cooperatively rasterize edges from the relevant CSR bands. let tile_top_fixed = band_top << FIXED_SHIFT; let tile_bottom_fixed = tile_top_fixed + (i32(16) << FIXED_SHIFT); @@ -838,8 +861,6 @@ fn cs_main( let csr_start = csr_offsets[command.csr_offsets_start + u32(band)]; let csr_end = csr_offsets[command.csr_offsets_start + u32(band) + 1u]; let band_edge_count = csr_end - csr_start; - // Clip to intersection of tile window and CSR band window - // to avoid double-counting edges that span multiple CSR bands. let csr_band_top_fixed = (band * 16) << FIXED_SHIFT; let csr_band_bottom_fixed = csr_band_top_fixed + (i32(16) << FIXED_SHIFT); let clip_top = max(tile_top_fixed, csr_band_top_fixed); @@ -859,12 +880,7 @@ fn cs_main( // Compute coverage and compose for this pixel. if in_bounds { - let cmd_min_x = bitcast(command.destination_x); - let cmd_min_y = bitcast(command.destination_y); - let cmd_max_x = cmd_min_x + i32(command.destination_width); - let cmd_max_y = cmd_min_y + i32(command.destination_height); if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { - // Prefix sum of cover deltas for this row. var cover = atomicLoad(&tile_start_cover[py]); for (var col = 0u; col < px; col++) { cover += atomicLoad(&tile_cover[py * 16u + col]); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixBlockScanComputeShader.cs similarity index 94% rename from src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixBlockScanComputeShader.cs index 8c06dfd0b..2b8dfc300 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixBlockScanComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixBlockScanComputeShader.cs @@ -4,11 +4,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Phase 2 of the parallel tile prefix sum: a single workgroup performs an +/// Phase 2 of the parallel CSR prefix sum: a single workgroup performs an /// in-place exclusive prefix sum over the block_sums array from phase 1. -/// Supports up to 65536 blocks (256 * 256 = 16M tiles). +/// Supports up to 65536 blocks (256 * 256 = 16M bands). /// -internal static class PreparedCompositeTilePrefixBlockScanComputeShader +internal static class CsrPrefixBlockScanComputeShader { private static readonly byte[] CodeBytes = [ diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixLocalComputeShader.cs similarity index 93% rename from src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixLocalComputeShader.cs index 24af1f654..383abfada 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixLocalComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixLocalComputeShader.cs @@ -4,11 +4,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Phase 1 of the parallel tile prefix sum: each workgroup computes a local -/// exclusive prefix sum over 256 tile counts, writes per-tile starts, and +/// Phase 1 of the parallel CSR prefix sum: each workgroup computes a local +/// exclusive prefix sum over 256 band counts, writes per-band offsets, and /// stores the workgroup total into a block_sums buffer. /// -internal static class PreparedCompositeTilePrefixLocalComputeShader +internal static class CsrPrefixLocalComputeShader { /// /// The number of tiles processed by each workgroup. diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixPropagateComputeShader.cs similarity index 89% rename from src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixPropagateComputeShader.cs index 81015edc1..1e310025a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixPropagateComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CsrPrefixPropagateComputeShader.cs @@ -4,11 +4,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Phase 3 of the parallel tile prefix sum: each workgroup adds its -/// block prefix from block_sums to all tile_starts in its range. +/// Phase 3 of the parallel CSR prefix sum: each workgroup adds its +/// block prefix from block_sums to all CSR offsets in its range. /// Workgroup 0 is skipped (its prefix is 0). /// -internal static class PreparedCompositeTilePrefixPropagateComputeShader +internal static class CsrPrefixPropagateComputeShader { private static readonly byte[] CodeBytes = [ diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs deleted file mode 100644 index 391552b81..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeBinningComputeShader.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Bins prepared composite commands into coarse bins for later tile dispatch. -/// Produces per-bin headers and a compact bin list for the tile count/fill passes. -/// -internal static class PreparedCompositeBinningComputeShader -{ - /// - /// Gets the null-terminated WGSL source for command binning. - /// - private static readonly byte[] CodeBytes = - [ - .. """ - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - width_in_bins: u32, - height_in_bins: u32, - bin_count: u32, - partition_count: u32, - binning_size: u32, - bin_data_start: u32, - }; - - struct CommandBbox { - x0: i32, - y0: i32, - x1: i32, - y1: i32, - }; - - struct BinHeader { - element_count: u32, - chunk_offset: u32, - }; - - struct BumpAllocators { - failed: atomic, - binning: atomic, - }; - - @group(0) @binding(0) var command_bboxes: array; - @group(0) @binding(1) var bin_header: array; - @group(0) @binding(2) var bin_data: array; - @group(0) @binding(3) var bump: BumpAllocators; - @group(0) @binding(4) var dispatch_config: DispatchConfig; - - const TILE_WIDTH: u32 = 16u; - const TILE_HEIGHT: u32 = 16u; - const N_TILE_X: u32 = 16u; - const N_TILE_Y: u32 = 16u; - const N_TILE: u32 = N_TILE_X * N_TILE_Y; - const WG_SIZE: u32 = 256u; - const N_SLICE: u32 = WG_SIZE / 32u; - const N_SUBSLICE: u32 = 4u; - const SX: f32 = 1.0 / f32(N_TILE_X * TILE_WIDTH); - const SY: f32 = 1.0 / f32(N_TILE_Y * TILE_HEIGHT); - const STAGE_BINNING: u32 = 1u; - - var sh_bitmaps: array, N_TILE>, N_SLICE>; - var sh_count: array, N_SUBSLICE>; - var sh_chunk_offset: array; - - @compute @workgroup_size(256) - fn cs_main( - @builtin(global_invocation_id) global_id: vec3, - @builtin(local_invocation_id) local_id: vec3, - ) { - for (var i = 0u; i < N_SLICE; i += 1u) { - atomicStore(&sh_bitmaps[i][local_id.x], 0u); - } - workgroupBarrier(); - - let element_ix = global_id.x; - var x0 = 0; - var y0 = 0; - var x1 = 0; - var y1 = 0; - if (element_ix < dispatch_config.command_count) { - let bbox = command_bboxes[element_ix]; - let fbbox = vec4(vec4(bbox.x0, bbox.y0, bbox.x1, bbox.y1)); - if (fbbox.x < fbbox.z && fbbox.y < fbbox.w) { - x0 = i32(floor(fbbox.x * SX)); - y0 = i32(floor(fbbox.y * SY)); - x1 = i32(ceil(fbbox.z * SX)); - y1 = i32(ceil(fbbox.w * SY)); - } - } - - let width_in_bins = i32(dispatch_config.width_in_bins); - let height_in_bins = i32(dispatch_config.height_in_bins); - x0 = clamp(x0, 0, width_in_bins); - y0 = clamp(y0, 0, height_in_bins); - x1 = clamp(x1, 0, width_in_bins); - y1 = clamp(y1, 0, height_in_bins); - if (x0 == x1) { - y1 = y0; - } - - var x = x0; - var y = y0; - let my_slice = local_id.x / 32u; - let my_mask = 1u << (local_id.x & 31u); - while y < y1 { - atomicOr(&sh_bitmaps[my_slice][u32(y * width_in_bins + x)], my_mask); - x += 1; - if x == x1 { - x = x0; - y += 1; - } - } - - workgroupBarrier(); - - var element_count = 0u; - for (var i = 0u; i < N_SUBSLICE; i += 1u) { - element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u][local_id.x])); - let element_count_lo = element_count; - element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u + 1u][local_id.x])); - let element_count_hi = element_count; - let element_count_packed = element_count_lo | (element_count_hi << 16u); - sh_count[i][local_id.x] = element_count_packed; - } - - var chunk_offset = atomicAdd(&bump.binning, element_count); - if chunk_offset + element_count > dispatch_config.binning_size { - chunk_offset = 0u; - atomicOr(&bump.failed, STAGE_BINNING); - } - - sh_chunk_offset[local_id.x] = chunk_offset; - bin_header[global_id.x].element_count = element_count; - bin_header[global_id.x].chunk_offset = chunk_offset; - workgroupBarrier(); - - x = x0; - y = y0; - while y < y1 { - let bin_ix = u32(y * width_in_bins + x); - let out_mask = atomicLoad(&sh_bitmaps[my_slice][bin_ix]); - if (out_mask & my_mask) != 0u { - var idx = countOneBits(out_mask & (my_mask - 1u)); - if my_slice > 0u { - let count_ix = my_slice - 1u; - let count_packed = sh_count[count_ix / 2u][bin_ix]; - idx += (count_packed >> (16u * (count_ix & 1u))) & 0xffffu; - } - let offset = dispatch_config.bin_data_start + sh_chunk_offset[bin_ix]; - bin_data[offset + idx] = element_ix; - } - x += 1; - if x == x1 { - x = x0; - y += 1; - } - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs deleted file mode 100644 index e90b02996..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileCountComputeShader.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Counts the number of composite commands affecting each tile using bin headers. -/// -internal static class PreparedCompositeTileCountComputeShader -{ - /// - /// Gets the null-terminated WGSL source for per-tile command counts. - /// - private static readonly byte[] CodeBytes = - [ - .. """ - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - width_in_bins: u32, - height_in_bins: u32, - bin_count: u32, - partition_count: u32, - binning_size: u32, - bin_data_start: u32, - }; - - struct CommandBbox { - x0: i32, - y0: i32, - x1: i32, - y1: i32, - }; - - struct BinHeader { - element_count: u32, - chunk_offset: u32, - }; - - @group(0) @binding(0) var command_bboxes: array; - @group(0) @binding(1) var bin_header: array; - @group(0) @binding(2) var bin_data: array; - @group(0) @binding(3) var tile_counts: array>; - @group(0) @binding(4) var dispatch_config: DispatchConfig; - - const TILE_WIDTH: u32 = 16u; - const TILE_HEIGHT: u32 = 16u; - const N_TILE_X: u32 = 16u; - const N_TILE_Y: u32 = 16u; - const N_TILE: u32 = N_TILE_X * N_TILE_Y; - - @compute @workgroup_size(256) - fn cs_main( - @builtin(local_invocation_id) local_id: vec3, - @builtin(workgroup_id) wg_id: vec3, - ) { - let bin_x = wg_id.x; - let bin_y = wg_id.y; - if (bin_x >= dispatch_config.width_in_bins || bin_y >= dispatch_config.height_in_bins) { - return; - } - - let tile_x = local_id.x % N_TILE_X; - let tile_y = local_id.x / N_TILE_X; - let global_tile_x = bin_x * N_TILE_X + tile_x; - let global_tile_y = bin_y * N_TILE_Y + tile_y; - if (global_tile_x >= dispatch_config.tile_count_x || global_tile_y >= dispatch_config.tile_count_y) { - return; - } - - let tile_index = global_tile_y * dispatch_config.tile_count_x + global_tile_x; - let tile_min_x = i32(global_tile_x * TILE_WIDTH); - let tile_min_y = i32(global_tile_y * TILE_HEIGHT); - let tile_max_x = tile_min_x + i32(TILE_WIDTH); - let tile_max_y = tile_min_y + i32(TILE_HEIGHT); - let bin_ix = bin_y * dispatch_config.width_in_bins + bin_x; - - var count = 0u; - var part_ix = 0u; - loop { - if (part_ix >= dispatch_config.partition_count) { - break; - } - - let header = bin_header[part_ix * N_TILE + bin_ix]; - let element_count = header.element_count; - let base = header.chunk_offset; - for (var i = 0u; i < element_count; i += 1u) { - let cmd_index = bin_data[dispatch_config.bin_data_start + base + i]; - let bbox = command_bboxes[cmd_index]; - if (bbox.x1 > tile_min_x && bbox.x0 < tile_max_x && bbox.y1 > tile_min_y && bbox.y0 < tile_max_y) { - count = count + 1u; - } - } - - part_ix = part_ix + 1u; - } - - atomicStore(&tile_counts[tile_index], count); - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs deleted file mode 100644 index 784a17369..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTileFillComputeShader.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Expands per-bin command lists into per-tile command indices after prefix sizing. -/// -internal static class PreparedCompositeTileFillComputeShader -{ - /// - /// Gets the null-terminated WGSL source for per-tile command index expansion. - /// - private static readonly byte[] CodeBytes = - [ - .. """ - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - width_in_bins: u32, - height_in_bins: u32, - bin_count: u32, - partition_count: u32, - binning_size: u32, - bin_data_start: u32, - }; - - struct CommandBbox { - x0: i32, - y0: i32, - x1: i32, - y1: i32, - }; - - struct BinHeader { - element_count: u32, - chunk_offset: u32, - }; - - @group(0) @binding(0) var command_bboxes: array; - @group(0) @binding(1) var bin_header: array; - @group(0) @binding(2) var bin_data: array; - @group(0) @binding(3) var tile_starts: array; - @group(0) @binding(4) var tile_counts: array>; - @group(0) @binding(5) var tile_command_indices: array; - @group(0) @binding(6) var dispatch_config: DispatchConfig; - - const TILE_WIDTH: u32 = 16u; - const TILE_HEIGHT: u32 = 16u; - const N_TILE_X: u32 = 16u; - const N_TILE_Y: u32 = 16u; - const N_TILE: u32 = N_TILE_X * N_TILE_Y; - - @compute @workgroup_size(256) - fn cs_main( - @builtin(local_invocation_id) local_id: vec3, - @builtin(workgroup_id) wg_id: vec3, - ) { - let bin_x = wg_id.x; - let bin_y = wg_id.y; - if (bin_x >= dispatch_config.width_in_bins || bin_y >= dispatch_config.height_in_bins) { - return; - } - - let tile_x = local_id.x % N_TILE_X; - let tile_y = local_id.x / N_TILE_X; - let global_tile_x = bin_x * N_TILE_X + tile_x; - let global_tile_y = bin_y * N_TILE_Y + tile_y; - if (global_tile_x >= dispatch_config.tile_count_x || global_tile_y >= dispatch_config.tile_count_y) { - return; - } - - let tile_index = global_tile_y * dispatch_config.tile_count_x + global_tile_x; - let tile_min_x = i32(global_tile_x * TILE_WIDTH); - let tile_min_y = i32(global_tile_y * TILE_HEIGHT); - let tile_max_x = tile_min_x + i32(TILE_WIDTH); - let tile_max_y = tile_min_y + i32(TILE_HEIGHT); - let bin_ix = bin_y * dispatch_config.width_in_bins + bin_x; - - let start = tile_starts[tile_index]; - var offset = 0u; - var part_ix = 0u; - loop { - if (part_ix >= dispatch_config.partition_count) { - break; - } - - let header = bin_header[part_ix * N_TILE + bin_ix]; - let element_count = header.element_count; - let base = header.chunk_offset; - for (var i = 0u; i < element_count; i += 1u) { - let cmd_index = bin_data[dispatch_config.bin_data_start + base + i]; - let bbox = command_bboxes[cmd_index]; - if (bbox.x1 > tile_min_x && bbox.x0 < tile_max_x && bbox.y1 > tile_min_y && bbox.y0 < tile_max_y) { - tile_command_indices[start + offset] = cmd_index; - offset = offset + 1u; - } - } - - part_ix = part_ix + 1u; - } - - atomicStore(&tile_counts[tile_index], offset); - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs deleted file mode 100644 index d3acb77e5..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PreparedCompositeTilePrefixComputeShader.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Prefix-sums per-tile command counts into tile starts for the fill pass. -/// -internal static class PreparedCompositeTilePrefixComputeShader -{ - /// - /// Gets the null-terminated WGSL source for tile prefix sum calculation. - /// - private static readonly byte[] CodeBytes = - [ - .. """ - struct DispatchConfig { - target_width: u32, - target_height: u32, - tile_count_x: u32, - tile_count_y: u32, - tile_count: u32, - command_count: u32, - source_origin_x: u32, - source_origin_y: u32, - output_origin_x: u32, - output_origin_y: u32, - width_in_bins: u32, - height_in_bins: u32, - bin_count: u32, - partition_count: u32, - binning_size: u32, - bin_data_start: u32, - }; - - @group(0) @binding(0) var tile_counts: array>; - @group(0) @binding(1) var tile_starts: array; - @group(0) @binding(2) var dispatch_config: DispatchConfig; - - @compute @workgroup_size(1, 1, 1) - fn cs_main(@builtin(global_invocation_id) global_id: vec3) { - if (global_id.x != 0u || global_id.y != 0u || global_id.z != 0u) { - return; - } - - var sum = 0u; - var tile_index = 0u; - loop { - if (tile_index >= dispatch_config.tile_count) { - break; - } - let count = atomicLoad(&tile_counts[tile_index]); - tile_starts[tile_index] = sum; - sum = sum + count; - tile_index = tile_index + 1u; - } - } - """u8, - 0 - ]; - - /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. - public static ReadOnlySpan Code => CodeBytes; -} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index a08324302..5672abadb 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -40,21 +40,9 @@ internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDi { private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; - private const int CompositeBinTileCountX = 16; - private const int CompositeBinTileCountY = 16; - private const int CompositeBinningWorkgroupSize = 256; - private const int CompositeBinWidth = CompositeTileWidth * CompositeBinTileCountX; - private const int CompositeBinHeight = CompositeTileHeight * CompositeBinTileCountY; private const uint PreparedBrushTypeSolid = 0; private const uint PreparedBrushTypeImage = 1; private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; - private const string PreparedCompositeCommandBboxesBufferKey = "prepared-composite/command-bboxes"; - private const string PreparedCompositeTileCountsBufferKey = "prepared-composite/tile-counts"; - private const string PreparedCompositeTileStartsBufferKey = "prepared-composite/tile-starts"; - private const string PreparedCompositeTileIndicesBufferKey = "prepared-composite/tile-indices"; - private const string PreparedCompositeBinHeaderBufferKey = "prepared-composite/bin-header"; - private const string PreparedCompositeBinDataBufferKey = "prepared-composite/bin-data"; - private const string PreparedCompositeBinningBumpBufferKey = "prepared-composite/binning-bump"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; private const int UniformBufferOffsetAlignment = 256; private const int CallbackTimeoutMilliseconds = 10_000; @@ -583,12 +571,12 @@ private bool TryDispatchPreparedCompositeCommands( return true; } - if (!PreparedCompositeFineComputeShader.TryGetCode(flushContext.TextureFormat, out byte[] shaderCode, out error)) + if (!CompositeComputeShader.TryGetCode(flushContext.TextureFormat, out byte[] shaderCode, out error)) { return false; } - if (!PreparedCompositeFineComputeShader.TryGetInputSampleType(flushContext.TextureFormat, out TextureSampleType inputTextureSampleType)) + if (!CompositeComputeShader.TryGetInputSampleType(flushContext.TextureFormat, out TextureSampleType inputTextureSampleType)) { error = $"Prepared composite fine shader does not support texture format '{flushContext.TextureFormat}'."; return false; @@ -596,7 +584,7 @@ private bool TryDispatchPreparedCompositeCommands( string pipelineKey = $"prepared-composite-fine/{flushContext.TextureFormat}"; bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) - => TryCreatePreparedCompositeFineBindGroupLayout( + => TryCreateCompositeBindGroupLayout( api, device, flushContext.TextureFormat, @@ -618,9 +606,6 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out int tileCountX = checked((int)DivideRoundUp(targetLocalBounds.Width, CompositeTileWidth)); int tileCountY = checked((int)DivideRoundUp(targetLocalBounds.Height, CompositeTileHeight)); int tileCount = checked(tileCountX * tileCountY); - int widthInBins = checked((int)DivideRoundUp(tileCountX, CompositeBinTileCountX)); - int heightInBins = checked((int)DivideRoundUp(tileCountY, CompositeBinTileCountY)); - int binCount = checked(widthInBins * heightInBins); if (tileCount == 0) { return true; @@ -629,18 +614,13 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out uint parameterSize = (uint)Unsafe.SizeOf(); IMemoryOwner parametersOwner = flushContext.MemoryAllocator.Allocate(commandCount); - IMemoryOwner bboxesOwner = - flushContext.MemoryAllocator.Allocate(commandCount); try { int flushCommandCount = commandCount; Span parameters = parametersOwner.Memory.Span[..commandCount]; - Span commandBboxes = bboxesOwner.Memory.Span[..commandCount]; TextureView* brushTextureView = backdropTextureView; nint brushTextureViewHandle = (nint)backdropTextureView; bool hasImageTexture = false; - uint maxTileCommandIndices = 0; - uint binningPairCount = 0; int commandIndex = 0; for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) @@ -723,19 +703,6 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out int edgeOriginX = destinationX - sourceOffset.X; int edgeOriginY = destinationY - sourceOffset.Y; - int minTileX = destinationX / CompositeTileWidth; - int minTileY = destinationY / CompositeTileHeight; - int maxTileX = (destinationX + destinationRegion.Width - 1) / CompositeTileWidth; - int maxTileY = (destinationY + destinationRegion.Height - 1) / CompositeTileHeight; - uint tileEmitCount = checked((uint)((maxTileX - minTileX + 1) * (maxTileY - minTileY + 1))); - maxTileCommandIndices = checked(maxTileCommandIndices + tileEmitCount); - - int minBinX = destinationX / CompositeBinWidth; - int minBinY = destinationY / CompositeBinHeight; - int maxBinX = (destinationX + destinationRegion.Width - 1) / CompositeBinWidth; - int maxBinY = (destinationY + destinationRegion.Height - 1) / CompositeBinHeight; - uint binEmitCount = checked((uint)((maxBinX - minBinX + 1) * (maxBinY - minBinY + 1))); - binningPairCount = checked(binningPairCount + binEmitCount); PreparedCompositeParameters commandParameters = new( destinationX, destinationY, @@ -760,11 +727,6 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out solidColor); parameters[commandIndex] = commandParameters; - commandBboxes[commandIndex] = new PreparedCompositeCommandBbox( - destinationX, - destinationY, - destinationX + destinationRegion.Width, - destinationY + destinationRegion.Height); commandIndex++; } } @@ -791,122 +753,6 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out (nuint)usedParameterByteCount); } - int usedCommandBboxByteCount = checked(flushCommandCount * Unsafe.SizeOf()); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeCommandBboxesBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)usedCommandBboxByteCount, - out WgpuBuffer* commandBboxesBuffer, - out _, - out error)) - { - return false; - } - - fixed (PreparedCompositeCommandBbox* usedBboxesPtr = commandBboxes) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - commandBboxesBuffer, - 0, - usedBboxesPtr, - (nuint)usedCommandBboxByteCount); - } - - int partitionCount = (int)DivideRoundUp(flushCommandCount, CompositeBinningWorkgroupSize); - uint binningSize = Math.Max(binningPairCount, 1u); - int binHeaderCount = checked(partitionCount * CompositeBinningWorkgroupSize); - int binHeaderByteCount = checked(binHeaderCount * Unsafe.SizeOf()); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeBinHeaderBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)binHeaderByteCount, - out WgpuBuffer* binHeaderBuffer, - out _, - out error)) - { - return false; - } - - nuint binDataByteCount = checked(binningSize * (nuint)sizeof(uint)); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeBinDataBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - binDataByteCount, - out WgpuBuffer* binDataBuffer, - out _, - out error)) - { - return false; - } - - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeBinningBumpBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)Unsafe.SizeOf(), - out WgpuBuffer* binningBumpBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - binningBumpBuffer, - 0, - (nuint)Unsafe.SizeOf()); - - int tileStartsByteCount = checked(tileCount * sizeof(uint)); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileStartsBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileStartsByteCount, - out WgpuBuffer* tileStartsBuffer, - out _, - out error)) - { - return false; - } - - int tileCountsByteCount = checked(tileCount * sizeof(uint)); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileCountsBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - (nuint)tileCountsByteCount, - out WgpuBuffer* tileCountsBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileStartsBuffer, - 0, - (nuint)tileStartsByteCount); - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - tileCountsBuffer, - 0, - (nuint)tileCountsByteCount); - - uint tileCommandCapacity = maxTileCommandIndices; - nuint usedTileCommandCount = Math.Max(tileCommandCapacity, 1u); - nuint tileCommandIndicesByteCount = checked(usedTileCommandCount * sizeof(uint)); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - PreparedCompositeTileIndicesBufferKey, - BufferUsage.Storage | BufferUsage.CopyDst, - tileCommandIndicesByteCount, - out WgpuBuffer* tileCommandIndicesBuffer, - out _, - out error)) - { - return false; - } - nuint dispatchConfigSize = (nuint)Unsafe.SizeOf(); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( PreparedCompositeDispatchConfigBufferKey, @@ -930,12 +776,12 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out (uint)sourceOriginY, (uint)outputOriginX, (uint)outputOriginY, - (uint)widthInBins, - (uint)heightInBins, - (uint)binCount, - (uint)partitionCount, - binningSize, - 0u); + 0, + 0, + 0, + 0, + 0, + 0); flushContext.Api.QueueWriteBuffer( flushContext.Queue, dispatchConfigBuffer, @@ -977,32 +823,22 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out CsrConfig csrConfig = new((uint)totalEdgeCount); flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrConfigBuffer, 0, &csrConfig, csrConfigSize); - // Clear band counts before the count dispatch. - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); + // CSR prefix sum config. + const int tilesPerWorkgroup = CsrPrefixLocalComputeShader.TilesPerWorkgroup; + int csrBlockCount = checked((int)DivideRoundUp(totalCsrEntries, tilesPerWorkgroup)); - // Dispatch CSR count: each thread processes one edge, atomicAdd to band_counts. - uint csrDispatchCount = DivideRoundUp(totalEdgeCount, CsrWorkgroupSize); - if (!this.DispatchComputePass( - flushContext, - "csr-count", - CsrCountComputeShader.Code, - TryCreateCsrCountBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrBandCountsBuffer, Offset = 0, Size = csrBandCountsByteCount }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; - return 3; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, csrDispatchCount, 1, 1), + nuint csrBlockSumsSize = checked((nuint)(Math.Max(csrBlockCount, 1) * sizeof(uint))); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "csr-prefix-block-sums", + BufferUsage.Storage | BufferUsage.CopyDst, + csrBlockSumsSize, + out WgpuBuffer* csrBlockSumsBuffer, + out _, out error)) { return false; } - // CSR prefix sum: exclusive prefix sum over band_counts → csr_offsets. - // Reuse the existing tile prefix infrastructure by providing a dispatch config - // with tile_count = totalCsrEntries. nuint csrPrefixConfigSize = (nuint)Unsafe.SizeOf(); if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( "csr-prefix-dispatch-config", @@ -1018,119 +854,164 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out PreparedCompositeDispatchConfig csrPrefixConfig = new(0, 0, 0, 0, (uint)totalCsrEntries, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrPrefixDispatchConfigBuffer, 0, &csrPrefixConfig, csrPrefixConfigSize); - // Dispatch tile prefix sum using band_counts as input and csr_offsets as output. - if (!this.DispatchPreparedCompositeTilePrefix( - flushContext, - csrBandCountsBuffer, - csrOffsetsBuffer, - csrPrefixDispatchConfigBuffer, - totalCsrEntries, - out error)) + WgpuBuffer* csrPrefixBlockConfigBuffer = null; + nuint csrPrefixBlockConfigSize = sizeof(uint); + if (csrBlockCount > 1) { - return false; + uint csrBlockCountValue = (uint)csrBlockCount; + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + "csr-prefix-block-config", + BufferUsage.Uniform | BufferUsage.CopyDst, + csrPrefixBlockConfigSize, + out csrPrefixBlockConfigBuffer, + out _, + out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrPrefixBlockConfigBuffer, 0, &csrBlockCountValue, csrPrefixBlockConfigSize); } - // Clear write cursors before scatter. + // All clears before the single CSR compute pass. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBlockSumsBuffer, 0, csrBlockSumsSize); flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrWriteCursorsBuffer, 0, csrWriteCursorsByteCount); - // Dispatch CSR scatter: each thread processes one edge, scatters into csr_indices. - if (!this.DispatchComputePass( - flushContext, - "csr-scatter", - CsrScatterComputeShader.Code, - TryCreateCsrScatterBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = csrOffsetsByteCount }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrWriteCursorsBuffer, Offset = 0, Size = csrWriteCursorsByteCount }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrIndicesBuffer, Offset = 0, Size = csrIndicesByteCount }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; - return 5; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, csrDispatchCount, 1, 1), - out error)) + // Single compute pass: CSR count → prefix sum (3 phases) → scatter. + uint csrDispatchCount = DivideRoundUp(totalEdgeCount, CsrWorkgroupSize); + ComputePassDescriptor csrPassDescriptor = default; + ComputePassEncoder* csrPass = flushContext.Api.CommandEncoderBeginComputePass( + flushContext.CommandEncoder, in csrPassDescriptor); + if (csrPass is null) { + error = "Failed to begin CSR compute pass."; return false; } - } - else - { - // No edges: clear CSR buffers. - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsByteCount); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesByteCount); - } - if (tileCommandCapacity > 0 && flushCommandCount > 0) - { - // Pass 1: Binning + tile count share a compute pass. + try { - ComputePassDescriptor setupPassDescriptor = default; - ComputePassEncoder* pass = flushContext.Api.CommandEncoderBeginComputePass( - flushContext.CommandEncoder, in setupPassDescriptor); - if (pass is null) + // CSR count: each thread processes one edge, atomicAdd to band_counts. + if (!this.DispatchIntoComputePass( + flushContext, + csrPass, + "csr-count", + CsrCountComputeShader.Code, + TryCreateCsrCountBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrBandCountsBuffer, Offset = 0, Size = csrBandCountsByteCount }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; + return 3; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, csrDispatchCount, 1, 1), + out error)) + { + return false; + } + + // CSR prefix local: per-workgroup prefix sum over band_counts → csr_offsets. + if (!this.DispatchIntoComputePass( + flushContext, + csrPass, + "csr-prefix-local", + CsrPrefixLocalComputeShader.Code, + TryCreateCsrPrefixLocalBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBandCountsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrPrefixDispatchConfigBuffer, Offset = 0, Size = csrPrefixConfigSize }; + return 4; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)csrBlockCount, 1, 1), + out error)) { - error = "Failed to begin binning/tile-count compute pass."; return false; } - try + if (csrBlockCount > 1) { - if (!this.DispatchPreparedCompositeBinningInto( + // CSR prefix block scan. + if (!this.DispatchIntoComputePass( flushContext, - pass, - commandBboxesBuffer, - binHeaderBuffer, - binDataBuffer, - binningBumpBuffer, - dispatchConfigBuffer, - flushCommandCount, - out error) || - !this.DispatchPreparedCompositeTileCountInto( + csrPass, + "csr-prefix-block-scan", + CsrPrefixBlockScanComputeShader.Code, + TryCreateCsrPrefixBlockScanBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrPrefixBlockConfigBuffer, Offset = 0, Size = csrPrefixBlockConfigSize }; + return 2; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, 1, 1, 1), + out error)) + { + return false; + } + + // CSR prefix propagate. + if (!this.DispatchIntoComputePass( flushContext, - pass, - commandBboxesBuffer, - binHeaderBuffer, - binDataBuffer, - tileCountsBuffer, - dispatchConfigBuffer, - widthInBins, - heightInBins, + csrPass, + "csr-prefix-propagate", + CsrPrefixPropagateComputeShader.Code, + TryCreateCsrPrefixPropagateBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrPrefixDispatchConfigBuffer, Offset = 0, Size = csrPrefixConfigSize }; + return 3; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)csrBlockCount, 1, 1), out error)) { return false; } } - finally + + // CSR scatter: each thread processes one edge, scatters into csr_indices. + if (!this.DispatchIntoComputePass( + flushContext, + csrPass, + "csr-scatter", + CsrScatterComputeShader.Code, + TryCreateCsrScatterBindGroupLayout, + (entries) => + { + entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; + entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = csrOffsetsByteCount }; + entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrWriteCursorsBuffer, Offset = 0, Size = csrWriteCursorsByteCount }; + entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrIndicesBuffer, Offset = 0, Size = csrIndicesByteCount }; + entries[4] = new BindGroupEntry { Binding = 4, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; + return 5; + }, + (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, csrDispatchCount, 1, 1), + out error)) { - flushContext.Api.ComputePassEncoderEnd(pass); - flushContext.Api.ComputePassEncoderRelease(pass); + return false; } } - - // Tile prefix sum needs a buffer clear between passes, then - // prefix phases + tile fill share a second compute pass. - if (!this.DispatchPreparedCompositeTilePrefixAndTileFill( - flushContext, - commandBboxesBuffer, - binHeaderBuffer, - binDataBuffer, - tileCountsBuffer, - tileStartsBuffer, - tileCommandIndicesBuffer, - dispatchConfigBuffer, - tileCount, - widthInBins, - heightInBins, - out error)) + finally { - return false; + flushContext.Api.ComputePassEncoderEnd(csrPass); + flushContext.Api.ComputePassEncoderRelease(csrPass); } } + else + { + // No edges: clear CSR buffers. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsByteCount); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesByteCount); + } // Fine composite dispatch with CSR buffers. - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[11]; + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -1163,41 +1044,20 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out bindGroupEntries[5] = new BindGroupEntry { Binding = 5, - Buffer = tileStartsBuffer, - Offset = 0, - Size = (nuint)tileStartsByteCount - }; - bindGroupEntries[6] = new BindGroupEntry - { - Binding = 6, - Buffer = tileCountsBuffer, - Offset = 0, - Size = (nuint)tileCountsByteCount - }; - bindGroupEntries[7] = new BindGroupEntry - { - Binding = 7, - Buffer = tileCommandIndicesBuffer, - Offset = 0, - Size = tileCommandIndicesByteCount - }; - bindGroupEntries[8] = new BindGroupEntry - { - Binding = 8, Buffer = dispatchConfigBuffer, Offset = 0, Size = dispatchConfigSize }; - bindGroupEntries[9] = new BindGroupEntry + bindGroupEntries[6] = new BindGroupEntry { - Binding = 9, + Binding = 6, Buffer = csrOffsetsBuffer, Offset = 0, Size = csrOffsetsByteCount }; - bindGroupEntries[10] = new BindGroupEntry + bindGroupEntries[7] = new BindGroupEntry { - Binding = 10, + Binding = 7, Buffer = csrIndicesBuffer, Offset = 0, Size = csrIndicesByteCount @@ -1206,7 +1066,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 11, + EntryCount = 8, Entries = bindGroupEntries }; @@ -1245,478 +1105,12 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out finally { parametersOwner.Dispose(); - bboxesOwner.Dispose(); } error = null; return true; } - private bool DispatchPreparedCompositeTileCount( - WebGPUFlushContext flushContext, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* dispatchConfigBuffer, - int widthInBins, - int heightInBins, - out string? error) - => this.DispatchComputePass( - flushContext, - "prepared-composite-tile-count", - PreparedCompositeTileCountComputeShader.Code, - TryCreatePreparedCompositeTileCountBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 5; - }, - (pass) => - { - uint workgroupCountX = (uint)widthInBins; - uint workgroupCountY = (uint)heightInBins; - if (workgroupCountX > 0 && workgroupCountY > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); - } - }, - out error); - - private bool DispatchPreparedCompositeBinning( - WebGPUFlushContext flushContext, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* binningBumpBuffer, - WgpuBuffer* dispatchConfigBuffer, - int commandCount, - out string? error) - => this.DispatchComputePass( - flushContext, - "prepared-composite-binning", - PreparedCompositeBinningComputeShader.Code, - TryCreatePreparedCompositeBinningBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = binningBumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 5; - }, - (pass) => - { - uint workgroupCount = DivideRoundUp(commandCount, CompositeBinningWorkgroupSize); - if (workgroupCount > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCount, 1, 1); - } - }, - out error); - - private bool DispatchPreparedCompositeTilePrefix( - WebGPUFlushContext flushContext, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* tileStartsBuffer, - WgpuBuffer* dispatchConfigBuffer, - int tileCount, - out string? error) - { - const int tilesPerWorkgroup = PreparedCompositeTilePrefixLocalComputeShader.TilesPerWorkgroup; - int blockCount = checked((int)DivideRoundUp(tileCount, tilesPerWorkgroup)); - - // Allocate block_sums buffer for inter-workgroup prefix propagation. - nuint blockSumsSize = checked((nuint)(Math.Max(blockCount, 1) * sizeof(uint))); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "prepared-composite-tile-prefix-block-sums", - BufferUsage.Storage | BufferUsage.CopyDst, - blockSumsSize, - out WgpuBuffer* blockSumsBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - blockSumsBuffer, - 0, - blockSumsSize); - - // Phase 1: Local prefix sum per workgroup + store block totals. - if (!this.DispatchComputePass( - flushContext, - "prepared-composite-tile-prefix-local", - PreparedCompositeTilePrefixLocalComputeShader.Code, - TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 4; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)blockCount, 1, 1), - out error)) - { - return false; - } - - if (blockCount <= 1) - { - // Single workgroup — no cross-block propagation needed. - return true; - } - - // Phase 2: Prefix sum over block_sums (single workgroup handles all blocks). - uint blockCountValue = (uint)blockCount; - nuint prefixConfigSize = sizeof(uint); - - // We need a small uniform buffer for the block count. - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "prepared-composite-tile-prefix-block-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - prefixConfigSize, - out WgpuBuffer* prefixConfigBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, prefixConfigBuffer, 0, &blockCountValue, prefixConfigSize); - - if (!this.DispatchComputePass( - flushContext, - "prepared-composite-tile-prefix-block-scan", - PreparedCompositeTilePrefixBlockScanComputeShader.Code, - TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = prefixConfigBuffer, Offset = 0, Size = prefixConfigSize }; - return 2; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1), - out error)) - { - return false; - } - - // Phase 3: Propagate block prefixes to tile_starts. - if (!this.DispatchComputePass( - flushContext, - "prepared-composite-tile-prefix-propagate", - PreparedCompositeTilePrefixPropagateComputeShader.Code, - TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 3; - }, - (pass) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, (uint)blockCount, 1, 1), - out error)) - { - return false; - } - - return true; - } - - private bool DispatchPreparedCompositeTileFill( - WebGPUFlushContext flushContext, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* tileStartsBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* tileCommandIndicesBuffer, - WgpuBuffer* dispatchConfigBuffer, - int widthInBins, - int heightInBins, - out string? error) - => this.DispatchComputePass( - flushContext, - "prepared-composite-tile-fill", - PreparedCompositeTileFillComputeShader.Code, - TryCreatePreparedCompositeTileFillBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[5] = new BindGroupEntry { Binding = 5, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[6] = new BindGroupEntry { Binding = 6, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 7; - }, - (pass) => - { - uint workgroupCountX = (uint)widthInBins; - uint workgroupCountY = (uint)heightInBins; - if (workgroupCountX > 0 && workgroupCountY > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); - } - }, - out error); - - private bool DispatchPreparedCompositeBinningInto( - WebGPUFlushContext flushContext, - ComputePassEncoder* passEncoder, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* binningBumpBuffer, - WgpuBuffer* dispatchConfigBuffer, - int commandCount, - out string? error) - => this.DispatchIntoComputePass( - flushContext, - passEncoder, - "prepared-composite-binning", - PreparedCompositeBinningComputeShader.Code, - TryCreatePreparedCompositeBinningBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = binningBumpBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 5; - }, - (pass) => - { - uint workgroupCount = DivideRoundUp(commandCount, CompositeBinningWorkgroupSize); - if (workgroupCount > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCount, 1, 1); - } - }, - out error); - - private bool DispatchPreparedCompositeTileCountInto( - WebGPUFlushContext flushContext, - ComputePassEncoder* passEncoder, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* dispatchConfigBuffer, - int widthInBins, - int heightInBins, - out string? error) - => this.DispatchIntoComputePass( - flushContext, - passEncoder, - "prepared-composite-tile-count", - PreparedCompositeTileCountComputeShader.Code, - TryCreatePreparedCompositeTileCountBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 5; - }, - (pass) => - { - uint workgroupCountX = (uint)widthInBins; - uint workgroupCountY = (uint)heightInBins; - if (workgroupCountX > 0 && workgroupCountY > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(pass, workgroupCountX, workgroupCountY, 1); - } - }, - out error); - - private bool DispatchPreparedCompositeTilePrefixAndTileFill( - WebGPUFlushContext flushContext, - WgpuBuffer* commandBboxesBuffer, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* binDataBuffer, - WgpuBuffer* tileCountsBuffer, - WgpuBuffer* tileStartsBuffer, - WgpuBuffer* tileCommandIndicesBuffer, - WgpuBuffer* dispatchConfigBuffer, - int tileCount, - int widthInBins, - int heightInBins, - out string? error) - { - const int tilesPerWorkgroup = PreparedCompositeTilePrefixLocalComputeShader.TilesPerWorkgroup; - int blockCount = checked((int)DivideRoundUp(tileCount, tilesPerWorkgroup)); - - // Allocate block_sums buffer for inter-workgroup prefix propagation. - nuint blockSumsSize = checked((nuint)(Math.Max(blockCount, 1) * sizeof(uint))); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "prepared-composite-tile-prefix-block-sums", - BufferUsage.Storage | BufferUsage.CopyDst, - blockSumsSize, - out WgpuBuffer* blockSumsBuffer, - out _, - out error)) - { - return false; - } - - // ClearBuffer must happen outside a compute pass. - flushContext.Api.CommandEncoderClearBuffer( - flushContext.CommandEncoder, - blockSumsBuffer, - 0, - blockSumsSize); - - // Prepare prefix config buffer (needed for multi-block case). - WgpuBuffer* prefixConfigBuffer = null; - nuint prefixConfigSize = sizeof(uint); - if (blockCount > 1) - { - uint blockCountValue = (uint)blockCount; - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "prepared-composite-tile-prefix-block-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - prefixConfigSize, - out prefixConfigBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, prefixConfigBuffer, 0, &blockCountValue, prefixConfigSize); - } - - // All prefix phases + tile fill share a single compute pass. - ComputePassDescriptor prefixPassDescriptor = default; - ComputePassEncoder* pass = flushContext.Api.CommandEncoderBeginComputePass( - flushContext.CommandEncoder, in prefixPassDescriptor); - if (pass is null) - { - error = "Failed to begin prefix/tile-fill compute pass."; - return false; - } - - try - { - // Phase 1: Local prefix sum per workgroup + store block totals. - if (!this.DispatchIntoComputePass( - flushContext, - pass, - "prepared-composite-tile-prefix-local", - PreparedCompositeTilePrefixLocalComputeShader.Code, - TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 4; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)blockCount, 1, 1), - out error)) - { - return false; - } - - if (blockCount > 1) - { - // Phase 2: Prefix sum over block_sums. - if (!this.DispatchIntoComputePass( - flushContext, - pass, - "prepared-composite-tile-prefix-block-scan", - PreparedCompositeTilePrefixBlockScanComputeShader.Code, - TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = prefixConfigBuffer, Offset = 0, Size = prefixConfigSize }; - return 2; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, 1, 1, 1), - out error)) - { - return false; - } - - // Phase 3: Propagate block prefixes to tile_starts. - if (!this.DispatchIntoComputePass( - flushContext, - pass, - "prepared-composite-tile-prefix-propagate", - PreparedCompositeTilePrefixPropagateComputeShader.Code, - TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = blockSumsBuffer, Offset = 0, Size = blockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 3; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)blockCount, 1, 1), - out error)) - { - return false; - } - } - - // Tile fill dispatched into the same pass. - if (!this.DispatchIntoComputePass( - flushContext, - pass, - "prepared-composite-tile-fill", - PreparedCompositeTileFillComputeShader.Code, - TryCreatePreparedCompositeTileFillBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = commandBboxesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = binHeaderBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = binDataBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = tileStartsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = tileCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[5] = new BindGroupEntry { Binding = 5, Buffer = tileCommandIndicesBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[6] = new BindGroupEntry { Binding = 6, Buffer = dispatchConfigBuffer, Offset = 0, Size = (nuint)Unsafe.SizeOf() }; - return 7; - }, - (p) => - { - uint workgroupCountX = (uint)widthInBins; - uint workgroupCountY = (uint)heightInBins; - if (workgroupCountX > 0 && workgroupCountY > 0) - { - flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, workgroupCountX, workgroupCountY, 1); - } - }, - out error)) - { - return false; - } - - return true; - } - finally - { - flushContext.Api.ComputePassEncoderEnd(pass); - flushContext.Api.ComputePassEncoderRelease(pass); - } - } - private static bool TryGetOrCreateImageTextureView( WebGPUFlushContext flushContext, Image image, @@ -1738,210 +1132,64 @@ private static bool TryGetOrCreateImageTextureView( Size = new Extent3D((uint)image.Width, (uint)image.Height, 1), Format = textureFormat, MipLevelCount = 1, - SampleCount = 1 - }; - - Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); - if (texture is null) - { - textureView = null; - error = "Failed to create image texture."; - return false; - } - - TextureViewDescriptor viewDescriptor = new() - { - Format = descriptor.Format, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - textureView = flushContext.Api.TextureCreateView(texture, in viewDescriptor); - if (textureView is null) - { - flushContext.Api.TextureRelease(texture); - error = "Failed to create image texture view."; - return false; - } - - flushContext.TrackTexture(texture); - flushContext.TrackTextureView(textureView); - flushContext.CacheSourceTextureView(image, textureView); - - Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); - WebGPUFlushContext.UploadTextureFromRegion( - flushContext.Api, - flushContext.Queue, - texture, - region, - flushContext.MemoryAllocator); - - error = null; - return true; - } - - /// - /// Creates the bind-group layout used by prepared composite compute shader. - /// - private static bool TryCreatePreparedCompositeFineBindGroupLayout( - WebGPU api, - Device* device, - TextureFormat outputTextureFormat, - TextureSampleType inputTextureSampleType, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[11]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = inputTextureSampleType, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout - { - SampleType = inputTextureSampleType, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - StorageTexture = new StorageTextureBindingLayout - { - Access = StorageTextureAccess.WriteOnly, - Format = outputTextureFormat, - ViewDimension = TextureViewDimension.Dimension2D - } - }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[5] = new BindGroupLayoutEntry - { - Binding = 5, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[6] = new BindGroupLayoutEntry - { - Binding = 6, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[7] = new BindGroupLayoutEntry - { - Binding = 7, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[8] = new BindGroupLayoutEntry - { - Binding = 8, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - entries[9] = new BindGroupLayoutEntry - { - Binding = 9, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[10] = new BindGroupLayoutEntry - { - Binding = 10, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } + SampleCount = 1 }; - BindGroupLayoutDescriptor descriptor = new() + Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in descriptor); + if (texture is null) { - EntryCount = 11, - Entries = entries + textureView = null; + error = "Failed to create image texture."; + return false; + } + + TextureViewDescriptor viewDescriptor = new() + { + Format = descriptor.Format, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All }; - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) + textureView = flushContext.Api.TextureCreateView(texture, in viewDescriptor); + if (textureView is null) { - error = "Failed to create prepared composite fine bind group layout."; + flushContext.Api.TextureRelease(texture); + error = "Failed to create image texture view."; return false; } + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + flushContext.CacheSourceTextureView(image, textureView); + + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + WebGPUFlushContext.UploadTextureFromRegion( + flushContext.Api, + flushContext.Queue, + texture, + region, + flushContext.MemoryAllocator); + error = null; return true; } - private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( + /// + /// Creates the bind-group layout used by prepared composite compute shader. + /// + private static bool TryCreateCompositeBindGroupLayout( WebGPU api, Device* device, + TextureFormat outputTextureFormat, + TextureSampleType inputTextureSampleType, out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1957,33 +1205,33 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( { Binding = 1, Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + Texture = new TextureBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false } }; entries[2] = new BindGroupLayoutEntry { Binding = 2, Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + Texture = new TextureBindingLayout { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false } }; entries[3] = new BindGroupLayoutEntry { Binding = 3, Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + StorageTexture = new StorageTextureBindingLayout { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 + Access = StorageTextureAccess.WriteOnly, + Format = outputTextureFormat, + ViewDimension = TextureViewDimension.Dimension2D } }; entries[4] = new BindGroupLayoutEntry @@ -1991,103 +1239,56 @@ private static bool TryCreatePreparedCompositeTileCountBindGroupLayout( Binding = 4, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 5, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create prepared composite tile-count bind group layout."; - return false; - } - - error = null; - return true; - } - - private static bool TryCreatePreparedCompositeBinningBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout { Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } }; - entries[1] = new BindGroupLayoutEntry + entries[5] = new BindGroupLayoutEntry { - Binding = 1, + Binding = 5, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.Uniform, HasDynamicOffset = false, - MinBindingSize = 0 + MinBindingSize = (nuint)Unsafe.SizeOf() } }; - entries[2] = new BindGroupLayoutEntry + entries[6] = new BindGroupLayoutEntry { - Binding = 2, + Binding = 6, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } }; - entries[3] = new BindGroupLayoutEntry + entries[7] = new BindGroupLayoutEntry { - Binding = 3, + Binding = 7, Visibility = ShaderStage.Compute, Buffer = new BufferBindingLayout { - Type = BufferBindingType.Storage, + Type = BufferBindingType.ReadOnlyStorage, HasDynamicOffset = false, MinBindingSize = 0 } }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, + EntryCount = 8, Entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create prepared composite binning bind group layout."; + error = "Failed to create prepared composite fine bind group layout."; return false; } @@ -2095,7 +1296,7 @@ private static bool TryCreatePreparedCompositeBinningBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout( + private static bool TryCreateCsrPrefixLocalBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, @@ -2164,7 +1365,7 @@ private static bool TryCreatePreparedCompositeTilePrefixLocalBindGroupLayout( return true; } - private static bool TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout( + private static bool TryCreateCsrPrefixBlockScanBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, @@ -2211,7 +1412,7 @@ private static bool TryCreatePreparedCompositeTilePrefixBlockScanBindGroupLayout return true; } - private static bool TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout( + private static bool TryCreateCsrPrefixPropagateBindGroupLayout( WebGPU api, Device* device, out BindGroupLayout* layout, @@ -2269,108 +1470,6 @@ private static bool TryCreatePreparedCompositeTilePrefixPropagateBindGroupLayout return true; } - private static bool TryCreatePreparedCompositeTileFillBindGroupLayout( - WebGPU api, - Device* device, - out BindGroupLayout* layout, - out string? error) - { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; - entries[0] = new BindGroupLayoutEntry - { - Binding = 0, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[1] = new BindGroupLayoutEntry - { - Binding = 1, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[2] = new BindGroupLayoutEntry - { - Binding = 2, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[3] = new BindGroupLayoutEntry - { - Binding = 3, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[4] = new BindGroupLayoutEntry - { - Binding = 4, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[5] = new BindGroupLayoutEntry - { - Binding = 5, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Storage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; - entries[6] = new BindGroupLayoutEntry - { - Binding = 6, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = (nuint)Unsafe.SizeOf() - } - }; - - BindGroupLayoutDescriptor descriptor = new() - { - EntryCount = 7, - Entries = entries - }; - - layout = api.DeviceCreateBindGroupLayout(device, in descriptor); - if (layout is null) - { - error = "Failed to create prepared composite tile-fill bind group layout."; - return false; - } - - error = null; - return true; - } - /// /// Creates one transient composition texture that can be rendered to, sampled from, and copied. /// @@ -2875,59 +1974,7 @@ public PreparedCompositeDispatchConfig( } /// - /// Integer bounding box for a prepared composite command in destination-local coordinates. - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct PreparedCompositeCommandBbox - { - public readonly int X0; - public readonly int Y0; - public readonly int X1; - public readonly int Y1; - - public PreparedCompositeCommandBbox(int x0, int y0, int x1, int y1) - { - this.X0 = x0; - this.Y0 = y0; - this.X1 = x1; - this.Y1 = y1; - } - } - - /// - /// Per-bin header describing the packed command list region. - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct PreparedCompositeBinHeader - { - public readonly uint ElementCount; - public readonly uint ChunkOffset; - - public PreparedCompositeBinHeader(uint elementCount, uint chunkOffset) - { - this.ElementCount = elementCount; - this.ChunkOffset = chunkOffset; - } - } - - /// - /// Bump allocator state for command binning. - /// - [StructLayout(LayoutKind.Sequential)] - private readonly struct PreparedCompositeBinningBump - { - public readonly uint Failed; - public readonly uint Binning; - - public PreparedCompositeBinningBump(uint failed, uint binning) - { - this.Failed = failed; - this.Binning = binning; - } - } - - /// - /// Prepared composite command parameters consumed by . + /// Prepared composite command parameters consumed by . /// Layout matches the WGSL Params struct exactly (24 u32 fields = 96 bytes). /// [StructLayout(LayoutKind.Sequential)] From 49ebbe6a2c9d81a5274781d5306526d370b7ef42 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 13:31:06 +1000 Subject: [PATCH 089/136] Add start_cover fast path; skip outside edges --- .../Shaders/CompositeComputeShader.cs | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 045abef14..9617fed40 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -709,6 +709,51 @@ fn clip_vertical(ex0: i32, ey0: i32, ex1: i32, ey1: i32, min_y: i32, max_y: i32) return ClippedEdge(rx0, ry0, rx1, ry1, 1); } + fn accumulate_start_cover(ey0: i32, ey1: i32, clip_top: i32, clip_bottom: i32, tile_top_fixed: i32) { + // Fast path for edges entirely left of the tile. + // Only start_cover is affected (no area). The total cover delta per row + // is the signed height of the edge within that row, which telescopes + // across columns. This avoids the full column-walking overhead. + var cy0 = clamp(ey0, clip_top, clip_bottom); + var cy1 = clamp(ey1, clip_top, clip_bottom); + if cy0 == cy1 { return; } + + let ly0 = cy0 - tile_top_fixed; + let ly1 = cy1 - tile_top_fixed; + + if ly0 < ly1 { + // Downward. + let row0 = ly0 >> FIXED_SHIFT; + let row1 = (ly1 - 1) >> FIXED_SHIFT; + let fy0 = ly0 - (row0 << FIXED_SHIFT); + let fy1 = ly1 - (row1 << FIXED_SHIFT); + if row0 == row1 { + atomicAdd(&tile_start_cover[row0], fy0 - fy1); + return; + } + atomicAdd(&tile_start_cover[row0], fy0 - FIXED_ONE); + for (var r = row0 + 1; r < row1; r++) { + atomicAdd(&tile_start_cover[r], -FIXED_ONE); + } + atomicAdd(&tile_start_cover[row1], -fy1); + } else { + // Upward. + let row0 = (ly0 - 1) >> FIXED_SHIFT; + let row1 = ly1 >> FIXED_SHIFT; + let fy0 = ly0 - (row0 << FIXED_SHIFT); + let fy1 = ly1 - (row1 << FIXED_SHIFT); + if row0 == row1 { + atomicAdd(&tile_start_cover[row0], fy0 - fy1); + return; + } + atomicAdd(&tile_start_cover[row0], fy0); + for (var r = row0 - 1; r > row1; r--) { + atomicAdd(&tile_start_cover[r], FIXED_ONE); + } + atomicAdd(&tile_start_cover[row1], FIXED_ONE - fy1); + } + } + fn rasterize_edge(edge: Edge, band_top: i32, band_left_fixed: i32, clip_top_fixed: i32, clip_bottom_fixed: i32) { let band_top_fixed = band_top << FIXED_SHIFT; let ex0 = edge.x0 - band_left_fixed; @@ -857,6 +902,7 @@ fn cs_main( // Cooperatively rasterize edges from the relevant CSR bands. let tile_top_fixed = band_top << FIXED_SHIFT; let tile_bottom_fixed = tile_top_fixed + (i32(16) << FIXED_SHIFT); + let tile_right_fixed = band_left_fixed + (i32(16) << FIXED_SHIFT); for (var band = first_band; band <= last_band; band++) { let csr_start = csr_offsets[command.csr_offsets_start + u32(band)]; let csr_end = csr_offsets[command.csr_offsets_start + u32(band) + 1u]; @@ -872,7 +918,18 @@ fn cs_main( } let edge_local_idx = csr_indices[csr_start + ei]; let edge = edges[command.edge_start + edge_local_idx]; - rasterize_edge(edge, band_top, band_left_fixed, clip_top, clip_bottom); + + // X-range spatial filter: skip edges that cannot affect this tile. + if min(edge.x0, edge.x1) >= tile_right_fixed { + // Edge entirely right of tile: no contribution. + } else if max(edge.x0, edge.x1) < band_left_fixed { + // Edge entirely left of tile: only affects start_cover. + accumulate_start_cover(edge.y0, edge.y1, clip_top, clip_bottom, tile_top_fixed); + } else { + // Edge overlaps tile: full rasterization. + rasterize_edge(edge, band_top, band_left_fixed, clip_top, clip_bottom); + } + ei += 256u; } } From 36425d1d89b8622a600e2debd3e969e8264ff83e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 19:16:15 +1000 Subject: [PATCH 090/136] Compute CSR on CPU; refactor coverage rasterizer --- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 447 ++++++++++++------ .../WebGPUDrawingBackend.cs | 261 ++-------- .../DrawingCanvasBatcher{TPixel}.cs | 13 +- .../Processing/DrawingCanvas{TPixel}.cs | 143 +----- 4 files changed, 334 insertions(+), 530 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 5e18f7768..7bb89452f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -2,10 +2,8 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Buffers.Binary; using System.Runtime.InteropServices; using Silk.NET.WebGPU; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -19,7 +17,6 @@ internal sealed unsafe partial class WebGPUDrawingBackend private const int FixedOne = 1 << FixedShift; private const int CsrWorkgroupSize = 256; - private readonly Dictionary coverageGeometryCache = []; private IMemoryOwner? cachedCoverageLineUpload; private int cachedCoverageLineLength; @@ -47,6 +44,10 @@ private bool TryCreateEdgeBuffer( out int totalEdgeCount, out int totalCsrEntries, out int totalCsrIndices, + out WgpuBuffer* csrOffsetsBuffer, + out nuint csrOffsetsBufferSize, + out WgpuBuffer* csrIndicesBuffer, + out nuint csrIndicesBufferSize, out string? error) where TPixel : unmanaged, IPixel { @@ -56,6 +57,10 @@ private bool TryCreateEdgeBuffer( totalEdgeCount = 0; totalCsrEntries = 0; totalCsrIndices = 0; + csrOffsetsBuffer = null; + csrOffsetsBufferSize = 0; + csrIndicesBuffer = null; + csrIndicesBufferSize = 0; error = null; if (definitions.Count == 0) { @@ -66,8 +71,8 @@ private bool TryCreateEdgeBuffer( int runningEdgeStart = 0; int runningCsrOffset = 0; - // First pass: resolve/build cached geometry and compute edge placements. - CachedCoverageGeometry?[] geometries = new CachedCoverageGeometry?[definitions.Count]; + // Build flattened geometry for each definition and compute edge placements. + DefinitionGeometry[] geometries = new DefinitionGeometry[definitions.Count]; for (int i = 0; i < definitions.Count; i++) { CompositionCoverageDefinition definition = definitions[i]; @@ -81,46 +86,39 @@ private bool TryCreateEdgeBuffer( uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); - CoverageDefinitionIdentity identity = new(definition); - if (!this.coverageGeometryCache.TryGetValue(identity, out CachedCoverageGeometry? geometry)) + if (!TryBuildFixedPointEdges( + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + out GpuEdge[]? defEdges, + out int edgeCount, + out int bandOverlaps, + out uint[]? defCsrOffsets, + out uint[]? defCsrIndices, + out error)) { - if (!TryBuildFixedPointEdges( - definition.Path, - in interest, - definition.RasterizerOptions.SamplingOrigin, - configuration.MemoryAllocator, - out IMemoryOwner? edgeOwner, - out int edgeCount, - out int bandOverlaps, - out error)) + // Return any already-built geometry arrays on failure. + for (int j = 0; j < i; j++) { - return false; + ReturnEdgeArray(geometries[j].Edges); } - geometry = new CachedCoverageGeometry(edgeOwner, edgeCount, bandCount, bandOverlaps); - this.coverageGeometryCache[identity] = geometry; - } - - if (geometry is null) - { - error = "Failed to resolve cached coverage geometry."; return false; } - geometries[i] = geometry; + geometries[i] = new DefinitionGeometry(defEdges, edgeCount, bandCount, bandOverlaps, defCsrOffsets, defCsrIndices); - // bandCount + 1 entries in CSR offsets (the +1 is the sentinel for the last band's end). int csrEntriesForDef = bandCount + 1; edgePlacements[i] = new EdgePlacement( (uint)runningEdgeStart, - (uint)geometry.EdgeCount, + (uint)edgeCount, fillRule, (uint)runningCsrOffset, (uint)bandCount); - runningEdgeStart += geometry.EdgeCount; + runningEdgeStart += edgeCount; runningCsrOffset += csrEntriesForDef; - totalCsrIndices += geometry.TotalBandOverlaps; + totalCsrIndices += bandOverlaps; } totalEdgeCount = runningEdgeStart; @@ -128,55 +126,80 @@ private bool TryCreateEdgeBuffer( if (totalEdgeCount == 0) { - // Provide a minimal buffer so the bind group is valid. + // Provide properly sized buffers even when there are no edges. + // totalCsrEntries may be > 0 (definitions exist with band counts) + // and the shader reads csr_offsets[csrOffsetsStart + band], so the + // buffer must be large enough and zeroed. edgeBufferSize = EdgeStrideBytes; - if (!TryGetOrCreateCoverageBuffer( - flushContext, - "coverage-aggregated-edges", - BufferUsage.Storage | BufferUsage.CopyDst, - edgeBufferSize, - out edgeBuffer, - out error)) + int emptyOffsetsCount = Math.Max(totalCsrEntries, 1); + int emptyIndicesCount = Math.Max(totalCsrIndices, 1); + csrOffsetsBufferSize = checked((nuint)(emptyOffsetsCount * sizeof(uint))); + csrIndicesBufferSize = checked((nuint)(emptyIndicesCount * sizeof(uint))); + if (!TryGetOrCreateCoverageBuffer(flushContext, "coverage-aggregated-edges", BufferUsage.Storage | BufferUsage.CopyDst, edgeBufferSize, out edgeBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsBufferSize, out csrOffsetsBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesBufferSize, out csrIndicesBuffer, out error)) { return false; } + // Zero the CSR buffers so the shader reads 0 edges per band. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsBufferSize); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesBufferSize); return true; } - // Build merged edge buffer with CSR metadata. + // Merge edge arrays and stamp per-definition metadata (CsrBandOffset, DefinitionEdgeStart). + // For single-definition scenes (common case), stamp in-place and upload directly. int edgeBufferBytes = checked(totalEdgeCount * EdgeStrideBytes); edgeBufferSize = (nuint)edgeBufferBytes; - using IMemoryOwner edgeUploadOwner = configuration.MemoryAllocator.Allocate(edgeBufferBytes); - Span edgeUpload = edgeUploadOwner.Memory.Span[..edgeBufferBytes]; - int mergedEdgeIndex = 0; - for (int defIndex = 0; defIndex < geometries.Length; defIndex++) + GpuEdge[]? mergedEdges; + bool mergedFromPool; + if (geometries.Length == 1 && geometries[0].Edges is not null) { - CachedCoverageGeometry? geometry = geometries[defIndex]; - if (geometry is null || geometry.EdgeCount == 0 || geometry.EdgeOwner is null) + // Single definition: stamp metadata directly into source array. + mergedEdges = geometries[0].Edges; + mergedFromPool = true; + EdgePlacement placement = edgePlacements[0]; + Span span = mergedEdges.AsSpan(0, totalEdgeCount); + for (int i = 0; i < span.Length; i++) { - continue; + span[i].CsrBandOffset = placement.CsrOffsetsStart; + span[i].DefinitionEdgeStart = placement.EdgeStart; } - - EdgePlacement placement = edgePlacements[defIndex]; - ReadOnlySpan sourceEdges = geometry.EdgeOwner.Memory.Span[..(geometry.EdgeCount * EdgeStrideBytes)]; - - for (int edgeIndex = 0; edgeIndex < geometry.EdgeCount; edgeIndex++) + } + else + { + // Multiple definitions: merge into a new array. + mergedEdges = ArrayPool.Shared.Rent(totalEdgeCount); + mergedFromPool = true; + int mergedEdgeIndex = 0; + for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { - int srcOffset = edgeIndex * EdgeStrideBytes; - int dstOffset = mergedEdgeIndex * EdgeStrideBytes; + ref DefinitionGeometry geometry = ref geometries[defIndex]; + if (geometry.EdgeCount == 0 || geometry.Edges is null) + { + continue; + } - // Copy x0, y0, x1, y1, min_row, max_row (24 bytes). - sourceEdges.Slice(srcOffset, 24).CopyTo(edgeUpload.Slice(dstOffset, 24)); + EdgePlacement placement = edgePlacements[defIndex]; + ReadOnlySpan source = geometry.Edges.AsSpan(0, geometry.EdgeCount); + Span dest = mergedEdges.AsSpan(mergedEdgeIndex, geometry.EdgeCount); + source.CopyTo(dest); - // Set csr_band_offset and definition_edge_start. - BinaryPrimitives.WriteUInt32LittleEndian(edgeUpload.Slice(dstOffset + 24, 4), placement.CsrOffsetsStart); - BinaryPrimitives.WriteUInt32LittleEndian(edgeUpload.Slice(dstOffset + 28, 4), placement.EdgeStart); - mergedEdgeIndex++; + for (int i = 0; i < dest.Length; i++) + { + dest[i].CsrBandOffset = placement.CsrOffsetsStart; + dest[i].DefinitionEdgeStart = placement.EdgeStart; + } + + mergedEdgeIndex += geometry.EdgeCount; } } + // Reinterpret typed array as bytes for GPU upload. + Span edgeUpload = MemoryMarshal.AsBytes(mergedEdges.AsSpan(0, totalEdgeCount)); + if (!TryGetOrCreateCoverageBuffer( flushContext, "coverage-aggregated-edges", @@ -185,6 +208,7 @@ private bool TryCreateEdgeBuffer( out edgeBuffer, out error)) { + ReturnGeometries(geometries, mergedEdges, mergedFromPool); return false; } @@ -196,9 +220,83 @@ private bool TryCreateEdgeBuffer( ref this.cachedCoverageLineLength, out error)) { + ReturnGeometries(geometries, mergedEdges, mergedFromPool); + return false; + } + + // Build merged CSR offsets and indices from pre-computed per-definition data. + int csrOffsetsCount = Math.Max(totalCsrEntries, 1); + int csrIndicesCount = Math.Max(totalCsrIndices, 1); + csrOffsetsBufferSize = checked((nuint)(csrOffsetsCount * sizeof(uint))); + csrIndicesBufferSize = checked((nuint)(csrIndicesCount * sizeof(uint))); + + if (!TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsBufferSize, out csrOffsetsBuffer, out error) || + !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesBufferSize, out csrIndicesBuffer, out error)) + { + ReturnGeometries(geometries, mergedEdges, mergedFromPool); return false; } + if (totalEdgeCount > 0 && totalCsrEntries > 0) + { + // Write merged CSR offsets and indices. Each definition's per-band offsets + // are shifted by a running base so indices from different definitions + // don't overlap in the global csr_indices array. + uint[] mergedOffsets = new uint[totalCsrEntries]; + uint[] mergedIndices = new uint[totalCsrIndices]; + uint runningIndicesBase = 0; + for (int defIndex = 0; defIndex < geometries.Length; defIndex++) + { + ref DefinitionGeometry geometry = ref geometries[defIndex]; + if (geometry.CsrOffsets is null || geometry.CsrIndices is null) + { + continue; + } + + EdgePlacement placement = edgePlacements[defIndex]; + int csrStart = (int)placement.CsrOffsetsStart; + uint[] defOffsets = geometry.CsrOffsets; + uint[] defIndices = geometry.CsrIndices; + + // Copy offsets shifted by running base (bandCount + 1 entries). + for (int b = 0; b < defOffsets.Length; b++) + { + mergedOffsets[csrStart + b] = defOffsets[b] + runningIndicesBase; + } + + // Copy indices. + defIndices.AsSpan().CopyTo(mergedIndices.AsSpan((int)runningIndicesBase)); + runningIndicesBase += (uint)defIndices.Length; + } + + fixed (uint* offsetsPtr = mergedOffsets) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + csrOffsetsBuffer, + 0, + offsetsPtr, + (nuint)(totalCsrEntries * sizeof(uint))); + } + + fixed (uint* indicesPtr = mergedIndices) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + csrIndicesBuffer, + 0, + indicesPtr, + (nuint)(totalCsrIndices * sizeof(uint))); + } + } + else + { + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsBufferSize); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesBufferSize); + } + + ReturnGeometries(geometries, mergedEdges, mergedFromPool); + error = null; return true; } @@ -313,16 +411,40 @@ private bool TryUploadDirtyCoverageRange( } /// - /// Releases cached coverage resources and clears all CPU-side upload caches. + /// Returns all pooled edge arrays from geometry entries. /// - private void DisposeCoverageResources() + private static void ReturnGeometries(DefinitionGeometry[] geometries, GpuEdge[]? mergedEdges, bool mergedFromPool) { - foreach (CachedCoverageGeometry geometry in this.coverageGeometryCache.Values) + for (int i = 0; i < geometries.Length; i++) + { + // For single-definition, Edges == mergedEdges; only return once. + if (geometries[i].Edges is not null && geometries[i].Edges != mergedEdges) + { + ArrayPool.Shared.Return(geometries[i].Edges!); + } + + geometries[i].Edges = null; + } + + if (mergedFromPool && mergedEdges is not null) { - geometry.Dispose(); + ArrayPool.Shared.Return(mergedEdges); } + } - this.coverageGeometryCache.Clear(); + /// + /// Returns a single edge array to the pool. + /// + private static void ReturnEdgeArray(GpuEdge[]? edges) + { + if (edges is not null) + { + ArrayPool.Shared.Return(edges); + } + } + + private void DisposeCoverageResources() + { this.cachedCoverageLineUpload?.Dispose(); this.cachedCoverageLineUpload = null; this.cachedCoverageLineLength = 0; @@ -336,71 +458,39 @@ private static bool TryBuildFixedPointEdges( IPath path, in Rectangle interest, RasterizerSamplingOrigin samplingOrigin, - MemoryAllocator allocator, - out IMemoryOwner? edgeOwner, + out GpuEdge[]? edges, out int edgeCount, out int totalBandOverlaps, + out uint[]? csrOffsets, + out uint[]? csrIndices, out string? error) { error = null; - edgeOwner = null; + edges = null; edgeCount = 0; totalBandOverlaps = 0; + csrOffsets = null; + csrIndices = null; bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; - - // First pass: count valid edges. - List simplePaths = []; - foreach (ISimplePath simplePath in path.Flatten()) - { - simplePaths.Add(simplePath); - } - - int maxEdgeCount = 0; - for (int i = 0; i < simplePaths.Count; i++) - { - ReadOnlySpan points = simplePaths[i].Points.Span; - if (points.Length < 2) - { - continue; - } - - int segmentCount = simplePaths[i].IsClosed ? points.Length : points.Length - 1; - if (segmentCount > 0) - { - maxEdgeCount += segmentCount; - } - } - - if (maxEdgeCount == 0) - { - return true; - } - int height = interest.Height; - int bufferBytes = checked(maxEdgeCount * EdgeStrideBytes); - IMemoryOwner tempOwner = allocator.Allocate(bufferBytes); - Span edgeBytes = tempOwner.Memory.Span[..bufferBytes]; - edgeBytes.Clear(); + // Single-pass: flatten path and write edges directly as typed structs. + // Use a pooled array that grows as needed. + GpuEdge[] edgeArray = ArrayPool.Shared.Rent(1024); int validEdgeCount = 0; int bandOverlaps = 0; - for (int i = 0; i < simplePaths.Count; i++) + + foreach (ISimplePath simplePath in path.Flatten()) { - ReadOnlySpan points = simplePaths[i].Points.Span; + ReadOnlySpan points = simplePath.Points.Span; if (points.Length < 2) { continue; } - bool contourClosed = simplePaths[i].IsClosed; - int segmentCount = contourClosed ? points.Length : points.Length - 1; - if (segmentCount <= 0) - { - continue; - } - + int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1; for (int j = 0; j < segmentCount; j++) { PointF p0 = points[j]; @@ -439,17 +529,25 @@ private static bool TryBuildFixedPointEdges( continue; } - // Write edge record (32 bytes). - int offset = validEdgeCount * EdgeStrideBytes; - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset, 4), x0); - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 4, 4), y0); - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 8, 4), x1); - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 12, 4), y1); - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 16, 4), minRow); - BinaryPrimitives.WriteInt32LittleEndian(edgeBytes.Slice(offset + 20, 4), maxRow); - int minBand = minRow / TileHeight; - int maxBand = maxRow / TileHeight; - bandOverlaps += maxBand - minBand + 1; + // Grow array if needed. + if (validEdgeCount == edgeArray.Length) + { + GpuEdge[] newArray = ArrayPool.Shared.Rent(edgeArray.Length * 2); + edgeArray.AsSpan(0, validEdgeCount).CopyTo(newArray); + ArrayPool.Shared.Return(edgeArray); + edgeArray = newArray; + } + + edgeArray[validEdgeCount] = new GpuEdge + { + X0 = x0, + Y0 = y0, + X1 = x1, + Y1 = y1, + MinRow = minRow, + MaxRow = maxRow, + }; + bandOverlaps += (maxRow / TileHeight) - (minRow / TileHeight) + 1; validEdgeCount++; } } @@ -459,11 +557,54 @@ private static bool TryBuildFixedPointEdges( if (validEdgeCount == 0) { - tempOwner.Dispose(); + ArrayPool.Shared.Return(edgeArray); return true; } - edgeOwner = tempOwner; + // Build CSR offsets and indices directly from struct fields. + int bandCount = (int)DivideRoundUp(height, TileHeight); + uint[] offsets = new uint[bandCount + 1]; + + // Count edges per band. + for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) + { + ref GpuEdge edge = ref edgeArray[edgeIdx]; + int minBand = edge.MinRow / TileHeight; + int maxBand = edge.MaxRow / TileHeight; + for (int b = minBand; b <= maxBand; b++) + { + offsets[b]++; + } + } + + // Exclusive prefix sum → offsets[i] = start index for band i. + uint running = 0; + for (int b = 0; b <= bandCount; b++) + { + uint count = offsets[b]; + offsets[b] = running; + running += count; + } + + // Scatter: write local edge indices into CSR index array. + uint[] indices = new uint[bandOverlaps]; + uint[] writeCursors = new uint[bandCount]; + for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) + { + ref GpuEdge edge = ref edgeArray[edgeIdx]; + int minBand = edge.MinRow / TileHeight; + int maxBand = edge.MaxRow / TileHeight; + for (int b = minBand; b <= maxBand; b++) + { + uint slot = offsets[b] + writeCursors[b]; + indices[slot] = (uint)edgeIdx; + writeCursors[b]++; + } + } + + csrOffsets = offsets; + csrIndices = indices; + edges = edgeArray; return true; } @@ -710,44 +851,48 @@ public CsrConfig(uint totalEdgeCount) } /// - /// Cached CPU-side geometry payload reused across coverage flushes. + /// GPU edge record matching the WGSL storage buffer layout (32 bytes, sequential). /// - private sealed class CachedCoverageGeometry : IDisposable + [StructLayout(LayoutKind.Sequential)] + private struct GpuEdge { - public CachedCoverageGeometry( - IMemoryOwner? edgeOwner, + public int X0; + public int Y0; + public int X1; + public int Y1; + public int MinRow; + public int MaxRow; + public uint CsrBandOffset; + public uint DefinitionEdgeStart; + } + + /// + /// Transient per-definition geometry produced during edge buffer construction. + /// + [StructLayout(LayoutKind.Auto)] + private struct DefinitionGeometry + { + public GpuEdge[]? Edges; + public int EdgeCount; + public int BandCount; + public int TotalBandOverlaps; + public uint[]? CsrOffsets; + public uint[]? CsrIndices; + + public DefinitionGeometry( + GpuEdge[]? edges, int edgeCount, int bandCount, - int totalBandOverlaps) + int totalBandOverlaps, + uint[]? csrOffsets, + uint[]? csrIndices) { - this.EdgeOwner = edgeOwner; + this.Edges = edges; this.EdgeCount = edgeCount; this.BandCount = bandCount; this.TotalBandOverlaps = totalBandOverlaps; + this.CsrOffsets = csrOffsets; + this.CsrIndices = csrIndices; } - - /// - /// Gets the owned fixed-point edge buffer. - /// - public IMemoryOwner? EdgeOwner { get; } - - /// - /// Gets the number of edges stored in . - /// - public int EdgeCount { get; } - - /// - /// Gets the number of 16-row CSR bands for this geometry. - /// - public int BandCount { get; } - - /// - /// Gets the total number of edge-band overlaps (for CSR indices sizing). - /// - public int TotalBandOverlaps { get; } - - /// - public void Dispose() - => this.EdgeOwner?.Dispose(); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 5672abadb..1addddd05 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -177,6 +177,9 @@ public void FlushCompositions( CompositionScene compositionScene) where TPixel : unmanaged, IPixel { +#if DEBUG_TIMING + long tMethodStart = Stopwatch.GetTimestamp(); +#endif this.ThrowIfDisposed(); if (compositionScene.Commands.Count == 0) { @@ -256,7 +259,8 @@ public void FlushCompositions( bool gpuReady = false; string? failure = null; int pixelSizeInBytes = Unsafe.SizeOf(); - using WebGPUFlushContext flushContext = WebGPUFlushContext.Create( + + WebGPUFlushContext flushContext = WebGPUFlushContext.Create( target, textureFormat, pixelSizeInBytes, @@ -269,21 +273,27 @@ public void FlushCompositions( this.TestingPrepareCoverageCallCount += commandCount; this.TestingReleaseCoverageCallCount += commandCount; - gpuSuccess = this.TryRenderPreparedFlush( + bool renderOk = this.TryRenderPreparedFlush( flushContext, preparedBatches, configuration, target.Bounds, compositionBounds.Value, commandCount, - out failure) && - this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); + out failure); + bool finalizeOk = renderOk && this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); + gpuSuccess = finalizeOk; } catch (Exception ex) { failure = ex.Message; gpuSuccess = false; } + finally + { + flushContext.Dispose(); + this.DisposeCoverageResources(); + } this.TestingGPUInitializationAttempted = true; this.TestingIsGPUReady = gpuReady; @@ -489,6 +499,10 @@ private bool TryRenderPreparedFlush( out int totalEdgeCount, out int totalCsrEntries, out int totalCsrIndices, + out WgpuBuffer* csrOffsetsBuffer, + out nuint csrOffsetsBufferSize, + out WgpuBuffer* csrIndicesBuffer, + out nuint csrIndicesBufferSize, out error)) { return false; @@ -510,9 +524,10 @@ private bool TryRenderPreparedFlush( edgePlacements, edgeBuffer, edgeBufferSize, - totalEdgeCount, - totalCsrEntries, - totalCsrIndices, + csrOffsetsBuffer, + csrOffsetsBufferSize, + csrIndicesBuffer, + csrIndicesBufferSize, out error)) { return false; @@ -559,9 +574,10 @@ private bool TryDispatchPreparedCompositeCommands( EdgePlacement[] edgePlacements, WgpuBuffer* edgeBuffer, nuint edgeBufferSize, - int totalEdgeCount, - int totalCsrEntries, - int totalCsrIndices, + WgpuBuffer* csrOffsetsBuffer, + nuint csrOffsetsBufferSize, + WgpuBuffer* csrIndicesBuffer, + nuint csrIndicesBufferSize, out string? error) where TPixel : unmanaged, IPixel { @@ -789,226 +805,11 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out &dispatchConfig, dispatchConfigSize); - // Build CSR (Compressed Sparse Row) edge bucketing on GPU. - int csrEntries = Math.Max(totalCsrEntries, 1); - int csrIndices = Math.Max(totalCsrIndices, 1); - nuint csrBandCountsByteCount = checked((nuint)(csrEntries * sizeof(uint))); - nuint csrOffsetsByteCount = csrBandCountsByteCount; - nuint csrWriteCursorsByteCount = csrBandCountsByteCount; - nuint csrIndicesByteCount = checked((nuint)(csrIndices * sizeof(uint))); - - if (!TryGetOrCreateCoverageBuffer(flushContext, "csr-band-counts", BufferUsage.Storage | BufferUsage.CopyDst, csrBandCountsByteCount, out WgpuBuffer* csrBandCountsBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsByteCount, out WgpuBuffer* csrOffsetsBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-write-cursors", BufferUsage.Storage | BufferUsage.CopyDst, csrWriteCursorsByteCount, out WgpuBuffer* csrWriteCursorsBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesByteCount, out WgpuBuffer* csrIndicesBuffer, out error)) - { - return false; - } - - if (totalEdgeCount > 0 && totalCsrEntries > 0) - { - // CSR config uniform. - nuint csrConfigSize = (nuint)Unsafe.SizeOf(); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "csr-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - csrConfigSize, - out WgpuBuffer* csrConfigBuffer, - out _, - out error)) - { - return false; - } - - CsrConfig csrConfig = new((uint)totalEdgeCount); - flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrConfigBuffer, 0, &csrConfig, csrConfigSize); - - // CSR prefix sum config. - const int tilesPerWorkgroup = CsrPrefixLocalComputeShader.TilesPerWorkgroup; - int csrBlockCount = checked((int)DivideRoundUp(totalCsrEntries, tilesPerWorkgroup)); - - nuint csrBlockSumsSize = checked((nuint)(Math.Max(csrBlockCount, 1) * sizeof(uint))); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "csr-prefix-block-sums", - BufferUsage.Storage | BufferUsage.CopyDst, - csrBlockSumsSize, - out WgpuBuffer* csrBlockSumsBuffer, - out _, - out error)) - { - return false; - } - - nuint csrPrefixConfigSize = (nuint)Unsafe.SizeOf(); - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "csr-prefix-dispatch-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - csrPrefixConfigSize, - out WgpuBuffer* csrPrefixDispatchConfigBuffer, - out _, - out error)) - { - return false; - } - - PreparedCompositeDispatchConfig csrPrefixConfig = new(0, 0, 0, 0, (uint)totalCsrEntries, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrPrefixDispatchConfigBuffer, 0, &csrPrefixConfig, csrPrefixConfigSize); - - WgpuBuffer* csrPrefixBlockConfigBuffer = null; - nuint csrPrefixBlockConfigSize = sizeof(uint); - if (csrBlockCount > 1) - { - uint csrBlockCountValue = (uint)csrBlockCount; - if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( - "csr-prefix-block-config", - BufferUsage.Uniform | BufferUsage.CopyDst, - csrPrefixBlockConfigSize, - out csrPrefixBlockConfigBuffer, - out _, - out error)) - { - return false; - } - - flushContext.Api.QueueWriteBuffer(flushContext.Queue, csrPrefixBlockConfigBuffer, 0, &csrBlockCountValue, csrPrefixBlockConfigSize); - } - - // All clears before the single CSR compute pass. - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBlockSumsBuffer, 0, csrBlockSumsSize); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrWriteCursorsBuffer, 0, csrWriteCursorsByteCount); - - // Single compute pass: CSR count → prefix sum (3 phases) → scatter. - uint csrDispatchCount = DivideRoundUp(totalEdgeCount, CsrWorkgroupSize); - ComputePassDescriptor csrPassDescriptor = default; - ComputePassEncoder* csrPass = flushContext.Api.CommandEncoderBeginComputePass( - flushContext.CommandEncoder, in csrPassDescriptor); - if (csrPass is null) - { - error = "Failed to begin CSR compute pass."; - return false; - } - - try - { - // CSR count: each thread processes one edge, atomicAdd to band_counts. - if (!this.DispatchIntoComputePass( - flushContext, - csrPass, - "csr-count", - CsrCountComputeShader.Code, - TryCreateCsrCountBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrBandCountsBuffer, Offset = 0, Size = csrBandCountsByteCount }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; - return 3; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, csrDispatchCount, 1, 1), - out error)) - { - return false; - } - - // CSR prefix local: per-workgroup prefix sum over band_counts → csr_offsets. - if (!this.DispatchIntoComputePass( - flushContext, - csrPass, - "csr-prefix-local", - CsrPrefixLocalComputeShader.Code, - TryCreateCsrPrefixLocalBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBandCountsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrPrefixDispatchConfigBuffer, Offset = 0, Size = csrPrefixConfigSize }; - return 4; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)csrBlockCount, 1, 1), - out error)) - { - return false; - } - - if (csrBlockCount > 1) - { - // CSR prefix block scan. - if (!this.DispatchIntoComputePass( - flushContext, - csrPass, - "csr-prefix-block-scan", - CsrPrefixBlockScanComputeShader.Code, - TryCreateCsrPrefixBlockScanBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrPrefixBlockConfigBuffer, Offset = 0, Size = csrPrefixBlockConfigSize }; - return 2; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, 1, 1, 1), - out error)) - { - return false; - } - - // CSR prefix propagate. - if (!this.DispatchIntoComputePass( - flushContext, - csrPass, - "csr-prefix-propagate", - CsrPrefixPropagateComputeShader.Code, - TryCreateCsrPrefixPropagateBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = csrBlockSumsBuffer, Offset = 0, Size = csrBlockSumsSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = nuint.MaxValue }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrPrefixDispatchConfigBuffer, Offset = 0, Size = csrPrefixConfigSize }; - return 3; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, (uint)csrBlockCount, 1, 1), - out error)) - { - return false; - } - } - - // CSR scatter: each thread processes one edge, scatters into csr_indices. - if (!this.DispatchIntoComputePass( - flushContext, - csrPass, - "csr-scatter", - CsrScatterComputeShader.Code, - TryCreateCsrScatterBindGroupLayout, - (entries) => - { - entries[0] = new BindGroupEntry { Binding = 0, Buffer = edgeBuffer, Offset = 0, Size = edgeBufferSize }; - entries[1] = new BindGroupEntry { Binding = 1, Buffer = csrOffsetsBuffer, Offset = 0, Size = csrOffsetsByteCount }; - entries[2] = new BindGroupEntry { Binding = 2, Buffer = csrWriteCursorsBuffer, Offset = 0, Size = csrWriteCursorsByteCount }; - entries[3] = new BindGroupEntry { Binding = 3, Buffer = csrIndicesBuffer, Offset = 0, Size = csrIndicesByteCount }; - entries[4] = new BindGroupEntry { Binding = 4, Buffer = csrConfigBuffer, Offset = 0, Size = csrConfigSize }; - return 5; - }, - (p) => flushContext.Api.ComputePassEncoderDispatchWorkgroups(p, csrDispatchCount, 1, 1), - out error)) - { - return false; - } - } - finally - { - flushContext.Api.ComputePassEncoderEnd(csrPass); - flushContext.Api.ComputePassEncoderRelease(csrPass); - } - } - else - { - // No edges: clear CSR buffers. - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrBandCountsBuffer, 0, csrBandCountsByteCount); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsByteCount); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesByteCount); - } + // CSR offsets and indices are pre-computed on CPU and uploaded directly. + // This eliminates the 5-dispatch GPU CSR pipeline (count → prefix-local → + // prefix-block-scan → prefix-propagate → scatter). + nuint csrOffsetsByteCount = csrOffsetsBufferSize; + nuint csrIndicesByteCount = csrIndicesBufferSize; // Fine composite dispatch with CSR buffers. BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 426e5be35..640c39c5e 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -21,7 +21,6 @@ internal sealed class DrawingCanvasBatcher private readonly IDrawingBackend backend; private readonly ICanvasFrame targetFrame; private readonly List commands = []; - private DrawingCanvasBatcher? mirrorBatcher; internal DrawingCanvasBatcher( Configuration configuration, @@ -38,17 +37,7 @@ internal DrawingCanvasBatcher( /// /// The command to queue. public void AddComposition(in CompositionCommand composition) - { - this.commands.Add(composition); - this.mirrorBatcher?.commands.Add(composition); - } - - /// - /// Sets an optional mirror batcher that receives the same queued commands. - /// - /// The mirror batcher, or to disable mirroring. - public void SetMirror(DrawingCanvasBatcher? mirrorBatcher) - => this.mirrorBatcher = mirrorBatcher; + => this.commands.Add(composition); /// /// Flushes queued commands to the backend as one scene packet, preserving submission order. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 2ceedda8a..afb1b8ce9 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -42,11 +42,6 @@ public sealed class DrawingCanvas : IDrawingCanvas /// private readonly DrawingCanvasBatcher batcher; - /// - /// Optional CPU shadow fallback used when target readback is unavailable. - /// - private readonly ShadowFallbackState? shadowFallback; - /// /// Temporary image resources that must stay alive until queued commands are flushed. /// @@ -113,8 +108,7 @@ internal DrawingCanvas( backend, targetFrame, new DrawingCanvasBatcher(configuration, backend, targetFrame), - new DrawingCanvasState(options, clipPaths), - CreateShadowFallbackIfNeeded(configuration, targetFrame)) + new DrawingCanvasState(options, clipPaths)) { } @@ -127,16 +121,12 @@ internal DrawingCanvas( /// The destination frame. /// The command batcher used for deferred composition. /// The default state used when no scoped state is active. - /// Optional shared shadow fallback state. - /// Whether to increment the shared shadow fallback reference count. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, DrawingCanvasBatcher batcher, - DrawingCanvasState defaultState, - ShadowFallbackState? shadowFallback = null, - bool addShadowReference = false) + DrawingCanvasState defaultState) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); @@ -153,13 +143,6 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; - this.shadowFallback = shadowFallback; - if (addShadowReference) - { - this.shadowFallback?.AddReference(); - } - - this.batcher.SetMirror(this.shadowFallback?.Batcher); // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); @@ -294,9 +277,7 @@ public DrawingCanvas CreateRegion(Rectangle region) this.backend, childFrame, this.batcher, - this.ResolveState(), - this.shadowFallback, - addShadowReference: true); + this.ResolveState()); } /// @@ -947,21 +928,7 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyListThe readback image when available. /// when source pixels were resolved. private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image? sourceImage) - { - if (this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage)) - { - return true; - } - - if (this.shadowFallback is not null) - { - sourceImage = this.shadowFallback.CloneRegion(sourceRect, this.configuration); - return true; - } - - sourceImage = null; - return false; - } + => this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage); /// /// Applies all clip paths to a subject path using the provided shape options. @@ -990,7 +957,6 @@ public void Flush() } finally { - this.shadowFallback?.Flush(); this.DisposePendingImageResources(); } } @@ -1009,16 +975,8 @@ public void Dispose() } finally { - try - { - this.shadowFallback?.Flush(); - } - finally - { - this.DisposePendingImageResources(); - this.shadowFallback?.Release(); - this.isDisposed = true; - } + this.DisposePendingImageResources(); + this.isDisposed = true; } } @@ -1127,30 +1085,6 @@ private CompositionCommand CreateCompositionCommand( definitionKeyCache); } - /// - /// Clones a rectangle from a CPU region into a new image. - /// - /// The source rectangle in local region coordinates. - /// The source CPU region. - /// The processing configuration. - /// A newly allocated image containing copied pixels from . - private static Image CloneRegionFromBuffer( - Rectangle sourceRect, - Buffer2DRegion sourceRegion, - Configuration configuration) - { - Image image = new(configuration, sourceRect.Width, sourceRect.Height); - Buffer2D destination = image.Frames.RootFrame.PixelBuffer; - for (int y = 0; y < sourceRect.Height; y++) - { - sourceRegion.DangerousGetRowSpan(sourceRect.Y + y) - .Slice(sourceRect.X, sourceRect.Width) - .CopyTo(destination.DangerousGetRowSpan(y)); - } - - return image; - } - /// /// Converts floating bounds to a conservative integer rectangle using floor/ceiling. /// @@ -1315,69 +1249,4 @@ private void DisposePendingImageResources() this.pendingImageResources.Clear(); } - - /// - /// Creates a shadow fallback state for non-CPU frame targets. - /// - /// The active processing configuration. - /// The canvas target frame. - /// A shadow fallback state when needed; otherwise . - private static ShadowFallbackState? CreateShadowFallbackIfNeeded( - Configuration configuration, - ICanvasFrame targetFrame) - { - bool hasCpuRegion = targetFrame.TryGetCpuRegion(out _); - bool hasNativeSurface = targetFrame.TryGetNativeSurface(out _); - if (hasCpuRegion || !hasNativeSurface) - { - return null; - } - - Image shadowImage = new(configuration, targetFrame.Bounds.Width, targetFrame.Bounds.Height); - Buffer2DRegion shadowRegion = new(shadowImage.Frames.RootFrame.PixelBuffer, targetFrame.Bounds); - ICanvasFrame shadowFrame = new CpuCanvasFrame(shadowRegion); - DrawingCanvasBatcher shadowBatcher = new(configuration, DefaultDrawingBackend.Instance, shadowFrame); - return new ShadowFallbackState(shadowImage, shadowBatcher); - } - - /// - /// Shared CPU shadow fallback state. - /// - private sealed class ShadowFallbackState - { - private int referenceCount = 1; - - public ShadowFallbackState(Image image, DrawingCanvasBatcher batcher) - { - this.Image = image; - this.Batcher = batcher; - } - - public Image Image { get; } - - public DrawingCanvasBatcher Batcher { get; } - - public void AddReference() - => this.referenceCount++; - - public void Release() - { - this.referenceCount--; - if (this.referenceCount > 0) - { - return; - } - - this.Image.Dispose(); - } - - public void Flush() - => this.Batcher.FlushCompositions(); - - public Image CloneRegion(Rectangle sourceRect, Configuration configuration) - { - Buffer2DRegion sourceRegion = new(this.Image.Frames.RootFrame.PixelBuffer, this.Image.Bounds); - return CloneRegionFromBuffer(sourceRect, sourceRegion, configuration); - } - } } From 5a3b89a7b9a0edd76febeadd06302bd3c1545eea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 19:21:06 +1000 Subject: [PATCH 091/136] Update DrawingCanvasTests.Process.cs --- .../Processing/DrawingCanvasTests.Process.cs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index 13cbe4642..c2a8c5d42 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -67,37 +67,6 @@ public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference(T target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } - [Theory] - [WithBlankImage(220, 160, PixelTypes.Rgba32)] - public void Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image target = provider.GetImage(); - IPath blurPath = CreateBlurEllipsePath(); - IPath pixelatePath = CreatePixelateTrianglePath(); - - Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); - CpuCanvasFrame proxyFrame = new(targetRegion); - MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame); - NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); - Configuration configuration = provider.Configuration.Clone(); - configuration.SetDrawingBackend(mirroringBackend); - - using (DrawingCanvas canvas = new( - configuration, - new NativeSurfaceOnlyFrame(target.Bounds, nativeSurface), - new DrawingOptions())) - { - DrawProcessScenario(canvas); - canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); - canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); - canvas.Flush(); - } - - target.DebugSave(provider, appendSourceFileOrDescription: false); - target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - [Fact] public void Process_UsesCanvasConfigurationForOperationContext() { From bb7311ec4b6cc5521dd6e12d2d87d5bc38aef7fb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 20:09:27 +1000 Subject: [PATCH 092/136] Switch edge buffers to IMemoryOwner --- .../WEBGPU_BACKEND_PROCESS.md | 168 +++++++++++----- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 181 +++++++++--------- 2 files changed, 202 insertions(+), 147 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 1e5a925d0..a6a70b4a0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -7,7 +7,7 @@ This document describes the current runtime flow used by `WebGPUDrawingBackend` ```text DrawingCanvasBatcher.Flush() -> IDrawingBackend.FlushCompositions(scene) - -> capability checks first + -> capability checks -> TryGetCompositeTextureFormat -> AreAllCompositionBrushesSupported -> if unsupported: scene-scoped fallback (DefaultDrawingBackend) @@ -18,69 +18,133 @@ DrawingCanvasBatcher.Flush() -> compute scene command count + composition bounds -> if no visible commands: return -> acquire one WebGPUFlushContext for the scene - -> ensure command encoder (single encoder reused for the scene) - -> resolve source backdrop texture view for composition bounds - -> non-readback path: sample target view directly - -> readback path: copy target region into transient source texture and sample that - -> allocate transient output texture for composition - -> build coverage texture from prepared geometry - -> flatten prepared path geometry - -> upload line/path/tile/segment buffers - -> run compute sequence: - 1) PathCountSetup - 2) PathCount - 3) Backdrop - 4) SegmentAlloc - 5) PathTilingSetup - 6) PathTiling - 7) CoverageFine - -> build one flush-scoped composite command parameter stream from prepared batches - -> run composite dispatch sequence: - 1) PreparedCompositeBinning - 2) PreparedCompositeTileCount - 3) PreparedCompositeTilePrefix - 4) PreparedCompositeTileFill - 5) PreparedCompositeFine - -> solid brush uses Color.ToScaledVector4() - -> image brush samples Image texture directly - -> writes composed pixels to one transient output texture - -> copy output texture bounds back into the destination target once - -> finalize once - -> non-readback: finish encoder + single queue submit - -> readback: encode texture->buffer copy, finish encoder + single queue submit, map/copy once - -> on any GPU failure path: scene-scoped fallback (DefaultDrawingBackend) + -> TryRenderPreparedFlush + -> ensure command encoder (single encoder reused for the scene) + -> use target texture view directly as backdrop source (no copy) + -> allocate transient output texture for composition bounds + -> deduplicate coverage definitions across batches via CoverageDefinitionIdentity + -> TryCreateEdgeBuffer (CPU-side edge preparation) + -> for each unique coverage definition: + -> path.Flatten() to iterate flattened vertices + -> build fixed-point (24.8) GpuEdge via MemoryAllocator (IMemoryOwner) + -> compute min_row/max_row per edge, clamped to interest + -> build CSR (Compressed Sparse Row) band-to-edge mapping: + 1) count edges per 16-row band + 2) exclusive prefix sum over band counts + 3) scatter edge indices into CSR index array + -> merge per-definition edges into single buffer with metadata stamps + -> single-definition fast path: stamp in-place + -> multi-definition: merge via Span.CopyTo + -> upload edge buffer via dirty-range detection (word-by-word diff) + -> upload merged CSR offsets and indices via QueueWriteBuffer + -> TryDispatchPreparedCompositeCommands + -> build per-command PreparedCompositeParameters (destination, edge placement, + brush type/color/region, blend mode, composition mode) + -> upload parameters + dispatch config via QueueWriteBuffer + -> single compute dispatch: CompositeComputeShader + -> workgroup size: 16x16 (one tile per workgroup) + -> dispatched as (tileCountX, tileCountY, 1) + -> each workgroup: + -> loads backdrop pixel from target texture + -> for each command overlapping this tile: + -> clears workgroup shared memory (tile_cover, tile_area, tile_start_cover) + -> cooperatively rasterizes edges from CSR bands using fixed-point scanline math + -> X-range spatial filter: edges left of tile only update start_cover + -> barrier, then each thread accumulates its coverage from shared memory + -> applies fill rule (non-zero or even-odd) + -> samples brush (solid color or image texture) + -> composes pixel using Porter-Duff alpha composition + color blend mode + -> writes final pixel to output texture + -> destination writeback: + -> NativeSurface: copy output texture region into target texture + -> CPU Region: set ReadbackSourceOverride to output texture (skip extra copy) + -> TryFinalizeFlush + -> NativeSurface: finish encoder + single QueueSubmit (non-blocking) + -> CPU Region: encode texture->buffer copy, finish encoder, QueueSubmit, + synchronous BufferMapAsync + poll wait, copy mapped bytes to CPU region + -> on any GPU failure: scene-scoped fallback (DefaultDrawingBackend) ``` +## GPU Buffer Layout + +### Edge Buffer (`coverage-aggregated-edges`) + +Each edge is a 32-byte `GpuEdge` struct (sequential layout): + +| Field | Type | Description | +|---|---|---| +| X0, Y0 | i32 | Start point in 24.8 fixed-point | +| X1, Y1 | i32 | End point in 24.8 fixed-point | +| MinRow | i32 | First pixel row touched (clamped to interest) | +| MaxRow | i32 | Last pixel row touched (clamped to interest) | +| CsrBandOffset | u32 | Start index into CSR offsets for this definition | +| DefinitionEdgeStart | u32 | Edge index offset for this definition in merged buffer | + +### CSR Buffers + +- `csr-offsets`: `array` — per-band prefix sum. `offsets[band]..offsets[band+1]` gives the range of edge indices for that 16-row band. +- `csr-indices`: `array` — edge indices within each band, ordered by band. + +### Command Parameters + +Each `PreparedCompositeParameters` struct contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, and blend percentage. + +### Dispatch Config + +`PreparedCompositeDispatchConfig` contains target dimensions, tile counts, source/output origins, and command count. + +## Shader Bindings (CompositeComputeShader) + +| Binding | Type | Description | +|---|---|---| +| 0 | `storage, read` | Edge buffer (`array`) | +| 1 | `texture_2d` | Backdrop texture (target) | +| 2 | `texture_2d` | Brush texture (image brush or same as backdrop) | +| 3 | `texture_storage_2d, write` | Output texture | +| 4 | `storage, read` | Command parameters (`array`) | +| 5 | `uniform` | Dispatch config | +| 6 | `storage, read` | CSR offsets (`array`) | +| 7 | `storage, read` | CSR indices (`array`) | + ## Context and Resource Lifetime -- `WebGPUFlushContext` is created once per `FlushCompositions` execution. -- The same command encoder is reused across all GPU passes in that flush. -- Transient textures/buffers/bind-groups are tracked in the flush context and released on dispose. -- Source image texture views are cached per flush context to avoid duplicate uploads. +- `WebGPUFlushContext` is created once per `FlushCompositions` execution and disposed at the end. +- The same command encoder is reused across all GPU operations in that flush. +- Transient textures, texture views, buffers, and bind groups are tracked in the flush context and released on dispose. +- Source image texture views are cached within the flush context to avoid duplicate uploads. +- CPU-side edge geometry (`IMemoryOwner`) is allocated via `MemoryAllocator` and disposed within the flush. +- Shared GPU buffers (edge buffer, CSR buffers, params buffer, dispatch config buffer) are managed by `DeviceState` with grow-only reuse across flushes. +- Edge upload uses dirty-range detection: compares current data word-by-word against a cached copy, uploading only the changed byte range via `QueueWriteBuffer`. -## Destination Writeback and Flush Count +## Destination Writeback - `FlushCompositions` performs one command-buffer submission (`QueueSubmit`) per scene flush. -- Destination writeback to the render target is one copy from the fine output texture into composition bounds. -- No destination storage init/blit pass is used in the active flush path. -- CPU-region targets perform one additional texture->buffer copy and one map/read after the single submit. +- NativeSurface targets: one GPU-side `CommandEncoderCopyTextureToTexture` from output into the target at composition bounds. No CPU stall. +- CPU Region targets: readback from the output texture directly (skipping the output-to-target copy). Uses `CommandEncoderCopyTextureToBuffer`, `QueueSubmit`, synchronous `BufferMapAsync` with device polling, then copies mapped bytes to the CPU `Buffer2DRegion`. ## Fallback Behavior -Fallback is scene-scoped: +Fallback is scene-scoped and triggered when: +- The pixel format has no supported WebGPU texture format mapping. +- Any command uses an unsupported brush type (only `SolidBrush` and `ImageBrush` are GPU-composable). +- Any GPU operation fails during the flush. + +Fallback path: +- If target exposes a CPU region: run `DefaultDrawingBackend.FlushCompositions(...)` directly. +- If target is native-surface only: rent CPU staging frame, run fallback on staging, upload staging pixels back to native target texture. + +## Shader Source + +`CompositeComputeShader` generates WGSL source per target texture format at runtime, substituting format-specific template tokens for texel decode/encode, backdrop/brush load, and output store. Generated source is cached by `TextureFormat` as null-terminated UTF-8 bytes. -- if target exposes a CPU region: - - run `DefaultDrawingBackend.FlushCompositions(...)` directly -- if target is native-surface only: - - rent CPU staging frame - - run `DefaultDrawingBackend.FlushCompositions(...)` on staging - - upload staging pixels back to native target texture +The following static WGSL shaders exist for the legacy CSR GPU pipeline but are not used in the current dispatch path (CSR is computed on CPU): +- `CsrCountComputeShader`, `CsrScatterComputeShader` +- `CsrPrefixLocalComputeShader`, `CsrPrefixBlockScanComputeShader`, `CsrPrefixPropagateComputeShader` -## Shader Source and Null Terminator +## Performance Characteristics -Static WGSL shaders are stored as null-terminated UTF-8 bytes (`U+0000` terminator required at call site), including: +Coverage rasterization and compositing are fused into a single compute dispatch. Each 16x16 tile workgroup computes coverage inline using a fixed-point scanline rasterizer ported from `DefaultRasterizer`, operating on workgroup shared memory with atomic accumulation. This eliminates the coverage texture, its allocation, write/read bandwidth, and the pass barrier that a separate coverage dispatch would require. -- coverage shaders: `PathCountSetup`, `PathCount`, `Backdrop`, `SegmentAlloc`, `PathTilingSetup`, `PathTiling`, `CoverageFine` -- prepared-composite shaders: `PreparedCompositeBinning`, `PreparedCompositeTileCount`, `PreparedCompositeTilePrefix`, `PreparedCompositeTileFill` +Edge preparation (path flattening, fixed-point conversion, CSR construction) runs on the CPU. The `path.Flatten()` cost is shared with the CPU rasterizer pipeline. CSR construction is three passes over the edge set: count, prefix sum, scatter. -`PreparedCompositeFine` is generated per target texture format and emitted as null-terminated UTF-8 bytes at runtime. +For the benchmark workload (7200x4800 US states GeoJSON polygon, 2px stroke, ~262K edges), NativeSurface performance is at parity with the CPU rasterizer (~28ms). diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 7bb89452f..6a43dc235 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Runtime.InteropServices; using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -87,26 +88,27 @@ private bool TryCreateEdgeBuffer( int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); if (!TryBuildFixedPointEdges( + flushContext.MemoryAllocator, definition.Path, in interest, definition.RasterizerOptions.SamplingOrigin, - out GpuEdge[]? defEdges, + out IMemoryOwner? defEdgeOwner, out int edgeCount, out int bandOverlaps, out uint[]? defCsrOffsets, out uint[]? defCsrIndices, out error)) { - // Return any already-built geometry arrays on failure. + // Dispose any already-built geometry on failure. for (int j = 0; j < i; j++) { - ReturnEdgeArray(geometries[j].Edges); + geometries[j].EdgeOwner?.Dispose(); } return false; } - geometries[i] = new DefinitionGeometry(defEdges, edgeCount, bandCount, bandOverlaps, defCsrOffsets, defCsrIndices); + geometries[i] = new DefinitionGeometry(defEdgeOwner, edgeCount, bandCount, bandOverlaps, defCsrOffsets, defCsrIndices); int csrEntriesForDef = bandCount + 1; edgePlacements[i] = new EdgePlacement( @@ -153,15 +155,13 @@ private bool TryCreateEdgeBuffer( int edgeBufferBytes = checked(totalEdgeCount * EdgeStrideBytes); edgeBufferSize = (nuint)edgeBufferBytes; - GpuEdge[]? mergedEdges; - bool mergedFromPool; - if (geometries.Length == 1 && geometries[0].Edges is not null) + IMemoryOwner? mergedEdgeOwner; + if (geometries.Length == 1 && geometries[0].EdgeOwner is not null) { - // Single definition: stamp metadata directly into source array. - mergedEdges = geometries[0].Edges; - mergedFromPool = true; + // Single definition: stamp metadata directly into source buffer. + mergedEdgeOwner = geometries[0].EdgeOwner!; EdgePlacement placement = edgePlacements[0]; - Span span = mergedEdges.AsSpan(0, totalEdgeCount); + Span span = mergedEdgeOwner.Memory.Span[..totalEdgeCount]; for (int i = 0; i < span.Length; i++) { span[i].CsrBandOffset = placement.CsrOffsetsStart; @@ -170,21 +170,20 @@ private bool TryCreateEdgeBuffer( } else { - // Multiple definitions: merge into a new array. - mergedEdges = ArrayPool.Shared.Rent(totalEdgeCount); - mergedFromPool = true; + // Multiple definitions: merge into a new buffer. + mergedEdgeOwner = flushContext.MemoryAllocator.Allocate(totalEdgeCount); int mergedEdgeIndex = 0; for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { ref DefinitionGeometry geometry = ref geometries[defIndex]; - if (geometry.EdgeCount == 0 || geometry.Edges is null) + if (geometry.EdgeCount == 0 || geometry.EdgeOwner is null) { continue; } EdgePlacement placement = edgePlacements[defIndex]; - ReadOnlySpan source = geometry.Edges.AsSpan(0, geometry.EdgeCount); - Span dest = mergedEdges.AsSpan(mergedEdgeIndex, geometry.EdgeCount); + ReadOnlySpan source = geometry.EdgeOwner.Memory.Span[..geometry.EdgeCount]; + Span dest = mergedEdgeOwner.Memory.Span.Slice(mergedEdgeIndex, geometry.EdgeCount); source.CopyTo(dest); for (int i = 0; i < dest.Length; i++) @@ -197,8 +196,8 @@ private bool TryCreateEdgeBuffer( } } - // Reinterpret typed array as bytes for GPU upload. - Span edgeUpload = MemoryMarshal.AsBytes(mergedEdges.AsSpan(0, totalEdgeCount)); + // Reinterpret typed buffer as bytes for GPU upload. + Span edgeUpload = MemoryMarshal.AsBytes(mergedEdgeOwner.Memory.Span[..totalEdgeCount]); if (!TryGetOrCreateCoverageBuffer( flushContext, @@ -208,7 +207,7 @@ private bool TryCreateEdgeBuffer( out edgeBuffer, out error)) { - ReturnGeometries(geometries, mergedEdges, mergedFromPool); + DisposeGeometries(geometries, mergedEdgeOwner); return false; } @@ -220,7 +219,7 @@ private bool TryCreateEdgeBuffer( ref this.cachedCoverageLineLength, out error)) { - ReturnGeometries(geometries, mergedEdges, mergedFromPool); + DisposeGeometries(geometries, mergedEdgeOwner); return false; } @@ -233,7 +232,7 @@ private bool TryCreateEdgeBuffer( if (!TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsBufferSize, out csrOffsetsBuffer, out error) || !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesBufferSize, out csrIndicesBuffer, out error)) { - ReturnGeometries(geometries, mergedEdges, mergedFromPool); + DisposeGeometries(geometries, mergedEdgeOwner); return false; } @@ -295,7 +294,7 @@ private bool TryCreateEdgeBuffer( flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesBufferSize); } - ReturnGeometries(geometries, mergedEdges, mergedFromPool); + DisposeGeometries(geometries, mergedEdgeOwner); error = null; return true; @@ -363,84 +362,73 @@ private bool TryUploadDirtyCoverageRange( firstDifferent++; } - if (firstDifferent < source.Length) + if (firstDifferent >= source.Length) { - int lastDifferent = source.Length - 1; - if (lastDifferent < commonLength) - { - while (lastDifferent >= firstDifferent && - lastDifferent >= commonAlignedLength && - cached[lastDifferent] == source[lastDifferent]) - { - lastDifferent--; - } - - int firstWordIndex = firstDifferent / sizeof(uint); - int lastWordIndex = Math.Min(lastDifferent / sizeof(uint), sourceWords.Length - 1); - while (lastWordIndex >= firstWordIndex && cachedWords[lastWordIndex] == sourceWords[lastWordIndex]) - { - lastWordIndex--; - } + // Data is identical to cache — skip upload and copy. + return true; + } - if (lastWordIndex >= firstWordIndex) - { - lastDifferent = Math.Min(lastDifferent, (lastWordIndex * sizeof(uint)) + (sizeof(uint) - 1)); - } + int lastDifferent = source.Length - 1; + if (lastDifferent < commonLength) + { + while (lastDifferent >= firstDifferent && + lastDifferent >= commonAlignedLength && + cached[lastDifferent] == source[lastDifferent]) + { + lastDifferent--; } - int uploadOffset = firstDifferent & ~0x3; - int uploadEnd = firstDifferent + (lastDifferent - firstDifferent) + 1; - uploadEnd = (uploadEnd + 3) & ~0x3; - uploadEnd = Math.Min(uploadEnd, source.Length); - int uploadLength = uploadEnd - uploadOffset; + int firstWordIndex = firstDifferent / sizeof(uint); + int lastWordIndex = Math.Min(lastDifferent / sizeof(uint), sourceWords.Length - 1); + while (lastWordIndex >= firstWordIndex && cachedWords[lastWordIndex] == sourceWords[lastWordIndex]) + { + lastWordIndex--; + } - fixed (byte* sourcePtr = source) + if (lastWordIndex >= firstWordIndex) { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - destinationBuffer, - (nuint)uploadOffset, - sourcePtr + uploadOffset, - (nuint)uploadLength); + lastDifferent = Math.Min(lastDifferent, (lastWordIndex * sizeof(uint)) + (sizeof(uint) - 1)); } } + int uploadOffset = firstDifferent & ~0x3; + int uploadEnd = firstDifferent + (lastDifferent - firstDifferent) + 1; + uploadEnd = (uploadEnd + 3) & ~0x3; + uploadEnd = Math.Min(uploadEnd, source.Length); + int uploadLength = uploadEnd - uploadOffset; + + fixed (byte* sourcePtr = source) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + destinationBuffer, + (nuint)uploadOffset, + sourcePtr + uploadOffset, + (nuint)uploadLength); + } + source.CopyTo(cached); cachedLength = source.Length; return true; } /// - /// Returns all pooled edge arrays from geometry entries. + /// Disposes all edge memory owners from geometry entries and the merged owner. /// - private static void ReturnGeometries(DefinitionGeometry[] geometries, GpuEdge[]? mergedEdges, bool mergedFromPool) + private static void DisposeGeometries(DefinitionGeometry[] geometries, IMemoryOwner? mergedEdgeOwner) { for (int i = 0; i < geometries.Length; i++) { - // For single-definition, Edges == mergedEdges; only return once. - if (geometries[i].Edges is not null && geometries[i].Edges != mergedEdges) + // For single-definition, EdgeOwner == mergedEdgeOwner; only dispose once. + if (geometries[i].EdgeOwner is not null && geometries[i].EdgeOwner != mergedEdgeOwner) { - ArrayPool.Shared.Return(geometries[i].Edges!); + geometries[i].EdgeOwner!.Dispose(); } - geometries[i].Edges = null; - } - - if (mergedFromPool && mergedEdges is not null) - { - ArrayPool.Shared.Return(mergedEdges); + geometries[i].EdgeOwner = null; } - } - /// - /// Returns a single edge array to the pool. - /// - private static void ReturnEdgeArray(GpuEdge[]? edges) - { - if (edges is not null) - { - ArrayPool.Shared.Return(edges); - } + mergedEdgeOwner?.Dispose(); } private void DisposeCoverageResources() @@ -455,10 +443,11 @@ private void DisposeCoverageResources() /// Each edge record is 32 bytes: x0, y0, x1, y1, min_row, max_row, 0, 0. /// private static bool TryBuildFixedPointEdges( + MemoryAllocator allocator, IPath path, in Rectangle interest, RasterizerSamplingOrigin samplingOrigin, - out GpuEdge[]? edges, + out IMemoryOwner? edgeOwner, out int edgeCount, out int totalBandOverlaps, out uint[]? csrOffsets, @@ -466,7 +455,7 @@ private static bool TryBuildFixedPointEdges( out string? error) { error = null; - edges = null; + edgeOwner = null; edgeCount = 0; totalBandOverlaps = 0; csrOffsets = null; @@ -477,8 +466,7 @@ private static bool TryBuildFixedPointEdges( int height = interest.Height; // Single-pass: flatten path and write edges directly as typed structs. - // Use a pooled array that grows as needed. - GpuEdge[] edgeArray = ArrayPool.Shared.Rent(1024); + IMemoryOwner currentOwner = allocator.Allocate(1024); int validEdgeCount = 0; int bandOverlaps = 0; @@ -529,16 +517,18 @@ private static bool TryBuildFixedPointEdges( continue; } - // Grow array if needed. - if (validEdgeCount == edgeArray.Length) + // Grow buffer if needed. + Span edgeSpan = currentOwner.Memory.Span; + if (validEdgeCount == edgeSpan.Length) { - GpuEdge[] newArray = ArrayPool.Shared.Rent(edgeArray.Length * 2); - edgeArray.AsSpan(0, validEdgeCount).CopyTo(newArray); - ArrayPool.Shared.Return(edgeArray); - edgeArray = newArray; + IMemoryOwner newOwner = allocator.Allocate(edgeSpan.Length * 2); + edgeSpan.CopyTo(newOwner.Memory.Span); + currentOwner.Dispose(); + currentOwner = newOwner; + edgeSpan = currentOwner.Memory.Span; } - edgeArray[validEdgeCount] = new GpuEdge + edgeSpan[validEdgeCount] = new GpuEdge { X0 = x0, Y0 = y0, @@ -557,18 +547,19 @@ private static bool TryBuildFixedPointEdges( if (validEdgeCount == 0) { - ArrayPool.Shared.Return(edgeArray); + currentOwner.Dispose(); return true; } // Build CSR offsets and indices directly from struct fields. + Span edges = currentOwner.Memory.Span; int bandCount = (int)DivideRoundUp(height, TileHeight); uint[] offsets = new uint[bandCount + 1]; // Count edges per band. for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) { - ref GpuEdge edge = ref edgeArray[edgeIdx]; + ref GpuEdge edge = ref edges[edgeIdx]; int minBand = edge.MinRow / TileHeight; int maxBand = edge.MaxRow / TileHeight; for (int b = minBand; b <= maxBand; b++) @@ -591,7 +582,7 @@ private static bool TryBuildFixedPointEdges( uint[] writeCursors = new uint[bandCount]; for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) { - ref GpuEdge edge = ref edgeArray[edgeIdx]; + ref GpuEdge edge = ref edges[edgeIdx]; int minBand = edge.MinRow / TileHeight; int maxBand = edge.MaxRow / TileHeight; for (int b = minBand; b <= maxBand; b++) @@ -604,7 +595,7 @@ private static bool TryBuildFixedPointEdges( csrOffsets = offsets; csrIndices = indices; - edges = edgeArray; + edgeOwner = currentOwner; return true; } @@ -872,7 +863,7 @@ private struct GpuEdge [StructLayout(LayoutKind.Auto)] private struct DefinitionGeometry { - public GpuEdge[]? Edges; + public IMemoryOwner? EdgeOwner; public int EdgeCount; public int BandCount; public int TotalBandOverlaps; @@ -880,14 +871,14 @@ private struct DefinitionGeometry public uint[]? CsrIndices; public DefinitionGeometry( - GpuEdge[]? edges, + IMemoryOwner? edgeOwner, int edgeCount, int bandCount, int totalBandOverlaps, uint[]? csrOffsets, uint[]? csrIndices) { - this.Edges = edges; + this.EdgeOwner = edgeOwner; this.EdgeCount = edgeCount; this.BandCount = bandCount; this.TotalBandOverlaps = totalBandOverlaps; From 8da2642e5fad8666d4f63163965f1557d3d38e68 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 21:52:17 +1000 Subject: [PATCH 093/136] Pre-split edges into band-sorted buffers (remove CSR) --- .../Shaders/CompositeComputeShader.cs | 56 +-- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 319 +++++++----------- .../WebGPUDrawingBackend.cs | 60 +--- .../Processing/Backends/DefaultRasterizer.cs | 192 +++-------- 4 files changed, 199 insertions(+), 428 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 9617fed40..4bf672c35 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -24,10 +24,6 @@ struct Edge { y0: i32, x1: i32, y1: i32, - min_row: i32, - max_row: i32, - csr_band_offset: u32, - definition_edge_start: u32, } struct Params { @@ -82,8 +78,7 @@ struct DispatchConfig { @group(0) @binding(3) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; @group(0) @binding(4) var commands: array; @group(0) @binding(5) var dispatch_config: DispatchConfig; - @group(0) @binding(6) var csr_offsets: array; - @group(0) @binding(7) var csr_indices: array; + @group(0) @binding(6) var band_offsets: array; // Workgroup shared memory for per-tile coverage accumulation. // Layout: 16 rows x 16 columns. Index = row * 16 + col. @@ -859,35 +854,24 @@ fn cs_main( // Determine this tile's position in coverage-local space. let band_top = tile_min_y - command.edge_origin_y; - let band_bottom = band_top + 16; let band_left_fixed = (tile_min_x - command.edge_origin_x) << FIXED_SHIFT; - // CSR band lookup: which 16-row bands overlap this tile? + // Band lookup: when edge_origin_y is 16-aligned the tile maps to one band; + // otherwise it can overlap two bands. var first_band = band_top / 16; if band_top < 0 && (band_top % 16) != 0 { first_band -= 1; } first_band = max(first_band, 0); - var last_band = (band_bottom - 1) / 16; - if band_bottom - 1 < 0 && ((band_bottom - 1) % 16) != 0 { - last_band -= 1; - } - last_band = min(last_band, i32(command.csr_band_count) - 1); + let last_band = min((band_top + 15) / 16, i32(command.csr_band_count) - 1); - // Early exit: skip if no CSR bands have edges for this tile (uniform). if first_band > last_band { continue; } - var tile_has_edges = false; - for (var b = first_band; b <= last_band; b++) { - let s = csr_offsets[command.csr_offsets_start + u32(b)]; - let e = csr_offsets[command.csr_offsets_start + u32(b) + 1u]; - if e > s { - tile_has_edges = true; - break; - } - } - if !tile_has_edges { + + let edge_range_start = band_offsets[command.csr_offsets_start + u32(first_band)]; + let edge_range_end = band_offsets[command.csr_offsets_start + u32(last_band) + 1u]; + if edge_range_start == edge_range_end { continue; } @@ -899,37 +883,33 @@ fn cs_main( } workgroupBarrier(); - // Cooperatively rasterize edges from the relevant CSR bands. + // Cooperatively rasterize edges from each overlapping band. let tile_top_fixed = band_top << FIXED_SHIFT; let tile_bottom_fixed = tile_top_fixed + (i32(16) << FIXED_SHIFT); let tile_right_fixed = band_left_fixed + (i32(16) << FIXED_SHIFT); + for (var band = first_band; band <= last_band; band++) { - let csr_start = csr_offsets[command.csr_offsets_start + u32(band)]; - let csr_end = csr_offsets[command.csr_offsets_start + u32(band) + 1u]; - let band_edge_count = csr_end - csr_start; - let csr_band_top_fixed = (band * 16) << FIXED_SHIFT; + let b_start = band_offsets[command.csr_offsets_start + u32(band)]; + let b_end = band_offsets[command.csr_offsets_start + u32(band) + 1u]; + let b_count = b_end - b_start; + + let csr_band_top_fixed = band * (i32(16) << FIXED_SHIFT); let csr_band_bottom_fixed = csr_band_top_fixed + (i32(16) << FIXED_SHIFT); let clip_top = max(tile_top_fixed, csr_band_top_fixed); let clip_bottom = min(tile_bottom_fixed, csr_band_bottom_fixed); + var ei = thread_id; loop { - if ei >= band_edge_count { + if ei >= b_count { break; } - let edge_local_idx = csr_indices[csr_start + ei]; - let edge = edges[command.edge_start + edge_local_idx]; - - // X-range spatial filter: skip edges that cannot affect this tile. + let edge = edges[command.edge_start + b_start + ei]; if min(edge.x0, edge.x1) >= tile_right_fixed { - // Edge entirely right of tile: no contribution. } else if max(edge.x0, edge.x1) < band_left_fixed { - // Edge entirely left of tile: only affects start_cover. accumulate_start_cover(edge.y0, edge.y1, clip_top, clip_bottom, tile_top_fixed); } else { - // Edge overlaps tile: full rasterization. rasterize_edge(edge, band_top, band_left_fixed, clip_top, clip_bottom); } - ei += 256u; } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 6a43dc235..2cf04478c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal sealed unsafe partial class WebGPUDrawingBackend { private const int TileHeight = 16; - private const int EdgeStrideBytes = 32; + private const int EdgeStrideBytes = 16; private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private const int CsrWorkgroupSize = 256; @@ -43,12 +43,9 @@ private bool TryCreateEdgeBuffer( out nuint edgeBufferSize, out EdgePlacement[] edgePlacements, out int totalEdgeCount, - out int totalCsrEntries, - out int totalCsrIndices, - out WgpuBuffer* csrOffsetsBuffer, - out nuint csrOffsetsBufferSize, - out WgpuBuffer* csrIndicesBuffer, - out nuint csrIndicesBufferSize, + out int totalBandOffsetEntries, + out WgpuBuffer* bandOffsetsBuffer, + out nuint bandOffsetsBufferSize, out string? error) where TPixel : unmanaged, IPixel { @@ -56,12 +53,9 @@ private bool TryCreateEdgeBuffer( edgeBufferSize = 0; edgePlacements = []; totalEdgeCount = 0; - totalCsrEntries = 0; - totalCsrIndices = 0; - csrOffsetsBuffer = null; - csrOffsetsBufferSize = 0; - csrIndicesBuffer = null; - csrIndicesBufferSize = 0; + totalBandOffsetEntries = 0; + bandOffsetsBuffer = null; + bandOffsetsBufferSize = 0; error = null; if (definitions.Count == 0) { @@ -70,9 +64,9 @@ private bool TryCreateEdgeBuffer( edgePlacements = new EdgePlacement[definitions.Count]; int runningEdgeStart = 0; - int runningCsrOffset = 0; + int runningBandOffset = 0; - // Build flattened geometry for each definition and compute edge placements. + // Build pre-split geometry for each definition and compute edge placements. DefinitionGeometry[] geometries = new DefinitionGeometry[definitions.Count]; for (int i = 0; i < definitions.Count; i++) { @@ -94,9 +88,7 @@ private bool TryCreateEdgeBuffer( definition.RasterizerOptions.SamplingOrigin, out IMemoryOwner? defEdgeOwner, out int edgeCount, - out int bandOverlaps, - out uint[]? defCsrOffsets, - out uint[]? defCsrIndices, + out uint[]? defBandOffsets, out error)) { // Dispose any already-built geometry on failure. @@ -108,69 +100,55 @@ private bool TryCreateEdgeBuffer( return false; } - geometries[i] = new DefinitionGeometry(defEdgeOwner, edgeCount, bandCount, bandOverlaps, defCsrOffsets, defCsrIndices); + geometries[i] = new DefinitionGeometry(defEdgeOwner, edgeCount, bandCount, defBandOffsets); - int csrEntriesForDef = bandCount + 1; + int bandOffsetEntriesForDef = bandCount + 1; edgePlacements[i] = new EdgePlacement( (uint)runningEdgeStart, (uint)edgeCount, fillRule, - (uint)runningCsrOffset, + (uint)runningBandOffset, (uint)bandCount); runningEdgeStart += edgeCount; - runningCsrOffset += csrEntriesForDef; - totalCsrIndices += bandOverlaps; + runningBandOffset += bandOffsetEntriesForDef; } totalEdgeCount = runningEdgeStart; - totalCsrEntries = runningCsrOffset; + totalBandOffsetEntries = runningBandOffset; if (totalEdgeCount == 0) { // Provide properly sized buffers even when there are no edges. - // totalCsrEntries may be > 0 (definitions exist with band counts) - // and the shader reads csr_offsets[csrOffsetsStart + band], so the + // The shader reads band_offsets[bandOffsetsStart + band], so the // buffer must be large enough and zeroed. edgeBufferSize = EdgeStrideBytes; - int emptyOffsetsCount = Math.Max(totalCsrEntries, 1); - int emptyIndicesCount = Math.Max(totalCsrIndices, 1); - csrOffsetsBufferSize = checked((nuint)(emptyOffsetsCount * sizeof(uint))); - csrIndicesBufferSize = checked((nuint)(emptyIndicesCount * sizeof(uint))); + int emptyOffsetsCount = Math.Max(totalBandOffsetEntries, 1); + bandOffsetsBufferSize = checked((nuint)(emptyOffsetsCount * sizeof(uint))); if (!TryGetOrCreateCoverageBuffer(flushContext, "coverage-aggregated-edges", BufferUsage.Storage | BufferUsage.CopyDst, edgeBufferSize, out edgeBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsBufferSize, out csrOffsetsBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesBufferSize, out csrIndicesBuffer, out error)) + !TryGetOrCreateCoverageBuffer(flushContext, "band-offsets", BufferUsage.Storage | BufferUsage.CopyDst, bandOffsetsBufferSize, out bandOffsetsBuffer, out error)) { return false; } - // Zero the CSR buffers so the shader reads 0 edges per band. - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsBufferSize); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesBufferSize); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, bandOffsetsBuffer, 0, bandOffsetsBufferSize); return true; } - // Merge edge arrays and stamp per-definition metadata (CsrBandOffset, DefinitionEdgeStart). - // For single-definition scenes (common case), stamp in-place and upload directly. + // Merge edge arrays. Edges are already pre-split and band-sorted per definition. + // For single-definition scenes (common case), use the buffer directly. int edgeBufferBytes = checked(totalEdgeCount * EdgeStrideBytes); edgeBufferSize = (nuint)edgeBufferBytes; IMemoryOwner? mergedEdgeOwner; if (geometries.Length == 1 && geometries[0].EdgeOwner is not null) { - // Single definition: stamp metadata directly into source buffer. + // Single definition: use directly (no per-edge metadata to stamp). mergedEdgeOwner = geometries[0].EdgeOwner!; - EdgePlacement placement = edgePlacements[0]; - Span span = mergedEdgeOwner.Memory.Span[..totalEdgeCount]; - for (int i = 0; i < span.Length; i++) - { - span[i].CsrBandOffset = placement.CsrOffsetsStart; - span[i].DefinitionEdgeStart = placement.EdgeStart; - } } else { - // Multiple definitions: merge into a new buffer. + // Multiple definitions: concatenate into a new buffer. mergedEdgeOwner = flushContext.MemoryAllocator.Allocate(totalEdgeCount); int mergedEdgeIndex = 0; for (int defIndex = 0; defIndex < geometries.Length; defIndex++) @@ -181,17 +159,9 @@ private bool TryCreateEdgeBuffer( continue; } - EdgePlacement placement = edgePlacements[defIndex]; ReadOnlySpan source = geometry.EdgeOwner.Memory.Span[..geometry.EdgeCount]; Span dest = mergedEdgeOwner.Memory.Span.Slice(mergedEdgeIndex, geometry.EdgeCount); source.CopyTo(dest); - - for (int i = 0; i < dest.Length; i++) - { - dest[i].CsrBandOffset = placement.CsrOffsetsStart; - dest[i].DefinitionEdgeStart = placement.EdgeStart; - } - mergedEdgeIndex += geometry.EdgeCount; } } @@ -223,75 +193,52 @@ private bool TryCreateEdgeBuffer( return false; } - // Build merged CSR offsets and indices from pre-computed per-definition data. - int csrOffsetsCount = Math.Max(totalCsrEntries, 1); - int csrIndicesCount = Math.Max(totalCsrIndices, 1); - csrOffsetsBufferSize = checked((nuint)(csrOffsetsCount * sizeof(uint))); - csrIndicesBufferSize = checked((nuint)(csrIndicesCount * sizeof(uint))); + // Build merged band offsets from pre-computed per-definition data. + // Band offsets are local to each definition's edge range (0-based). + int bandOffsetsCount = Math.Max(totalBandOffsetEntries, 1); + bandOffsetsBufferSize = checked((nuint)(bandOffsetsCount * sizeof(uint))); - if (!TryGetOrCreateCoverageBuffer(flushContext, "csr-offsets", BufferUsage.Storage | BufferUsage.CopyDst, csrOffsetsBufferSize, out csrOffsetsBuffer, out error) || - !TryGetOrCreateCoverageBuffer(flushContext, "csr-indices", BufferUsage.Storage | BufferUsage.CopyDst, csrIndicesBufferSize, out csrIndicesBuffer, out error)) + if (!TryGetOrCreateCoverageBuffer(flushContext, "band-offsets", BufferUsage.Storage | BufferUsage.CopyDst, bandOffsetsBufferSize, out bandOffsetsBuffer, out error)) { DisposeGeometries(geometries, mergedEdgeOwner); return false; } - if (totalEdgeCount > 0 && totalCsrEntries > 0) + if (totalEdgeCount > 0 && totalBandOffsetEntries > 0) { - // Write merged CSR offsets and indices. Each definition's per-band offsets - // are shifted by a running base so indices from different definitions - // don't overlap in the global csr_indices array. - uint[] mergedOffsets = new uint[totalCsrEntries]; - uint[] mergedIndices = new uint[totalCsrIndices]; - uint runningIndicesBase = 0; + uint[] mergedOffsets = new uint[totalBandOffsetEntries]; for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { ref DefinitionGeometry geometry = ref geometries[defIndex]; - if (geometry.CsrOffsets is null || geometry.CsrIndices is null) + if (geometry.BandOffsets is null) { continue; } EdgePlacement placement = edgePlacements[defIndex]; - int csrStart = (int)placement.CsrOffsetsStart; - uint[] defOffsets = geometry.CsrOffsets; - uint[] defIndices = geometry.CsrIndices; + int bandStart = (int)placement.CsrOffsetsStart; + uint[] defOffsets = geometry.BandOffsets; - // Copy offsets shifted by running base (bandCount + 1 entries). + // Copy band offsets directly (already 0-based per definition). for (int b = 0; b < defOffsets.Length; b++) { - mergedOffsets[csrStart + b] = defOffsets[b] + runningIndicesBase; + mergedOffsets[bandStart + b] = defOffsets[b]; } - - // Copy indices. - defIndices.AsSpan().CopyTo(mergedIndices.AsSpan((int)runningIndicesBase)); - runningIndicesBase += (uint)defIndices.Length; } fixed (uint* offsetsPtr = mergedOffsets) { flushContext.Api.QueueWriteBuffer( flushContext.Queue, - csrOffsetsBuffer, + bandOffsetsBuffer, 0, offsetsPtr, - (nuint)(totalCsrEntries * sizeof(uint))); - } - - fixed (uint* indicesPtr = mergedIndices) - { - flushContext.Api.QueueWriteBuffer( - flushContext.Queue, - csrIndicesBuffer, - 0, - indicesPtr, - (nuint)(totalCsrIndices * sizeof(uint))); + (nuint)(totalBandOffsetEntries * sizeof(uint))); } } else { - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrOffsetsBuffer, 0, csrOffsetsBufferSize); - flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, csrIndicesBuffer, 0, csrIndicesBufferSize); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, bandOffsetsBuffer, 0, bandOffsetsBufferSize); } DisposeGeometries(geometries, mergedEdgeOwner); @@ -439,9 +386,15 @@ private void DisposeCoverageResources() } /// - /// Flattens a path into fixed-point (24.8) edge format for GPU rasterization. - /// Each edge record is 32 bytes: x0, y0, x1, y1, min_row, max_row, 0, 0. + /// Flattens a path into fixed-point (24.8) edges placed into per-band regions. + /// Edges spanning multiple bands are duplicated (not clipped) so each band's + /// range is contiguous. Band offsets provide direct indexing into the edge buffer. + /// The shader handles per-tile Y clipping via clip_vertical. /// + /// + /// Uses a two-pass approach over the flattened path to avoid an intermediate buffer: + /// pass 1 counts edges per band, pass 2 scatters directly into the final buffer. + /// private static bool TryBuildFixedPointEdges( MemoryAllocator allocator, IPath path, @@ -449,26 +402,24 @@ private static bool TryBuildFixedPointEdges( RasterizerSamplingOrigin samplingOrigin, out IMemoryOwner? edgeOwner, out int edgeCount, - out int totalBandOverlaps, - out uint[]? csrOffsets, - out uint[]? csrIndices, + out uint[]? bandOffsets, out string? error) { error = null; edgeOwner = null; edgeCount = 0; - totalBandOverlaps = 0; - csrOffsets = null; - csrIndices = null; + bandOffsets = null; bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; int height = interest.Height; + int interestX = interest.X; + int interestY = interest.Y; + int bandCount = (int)DivideRoundUp(height, TileHeight); - // Single-pass: flatten path and write edges directly as typed structs. - IMemoryOwner currentOwner = allocator.Allocate(1024); - int validEdgeCount = 0; - int bandOverlaps = 0; + // Pass 1: Flatten path and count edges per band. + int totalSubEdges = 0; + int[] bandCounts = new int[bandCount]; foreach (ISimplePath simplePath in path.Flatten()) { @@ -482,120 +433,106 @@ private static bool TryBuildFixedPointEdges( for (int j = 0; j < segmentCount; j++) { PointF p0 = points[j]; - int nextIndex = j + 1; - if (nextIndex == points.Length) - { - nextIndex = 0; - } + PointF p1 = points[j + 1 == points.Length ? 0 : j + 1]; - PointF p1 = points[nextIndex]; - float fx0 = (p0.X - interest.X) + samplingOffsetX; - float fy0 = (p0.Y - interest.Y) + samplingOffsetY; - float fx1 = (p1.X - interest.X) + samplingOffsetX; - float fy1 = (p1.Y - interest.Y) + samplingOffsetY; - - // Convert to 24.8 fixed-point. - int x0 = (int)MathF.Round(fx0 * FixedOne); - int y0 = (int)MathF.Round(fy0 * FixedOne); - int x1 = (int)MathF.Round(fx1 * FixedOne); - int y1 = (int)MathF.Round(fy1 * FixedOne); - - // Skip horizontal edges (no coverage contribution). + int y0 = (int)MathF.Round(((p0.Y - interestY) + samplingOffsetY) * FixedOne); + int y1 = (int)MathF.Round(((p1.Y - interestY) + samplingOffsetY) * FixedOne); if (y0 == y1) { continue; } - // Compute min/max row (pixel coordinates), clamped to interest. int yMinFixed = Math.Min(y0, y1); int yMaxFixed = Math.Max(y0, y1); int minRow = Math.Max(0, yMinFixed >> FixedShift); int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); - if (minRow > maxRow) { continue; } - // Grow buffer if needed. - Span edgeSpan = currentOwner.Memory.Span; - if (validEdgeCount == edgeSpan.Length) + int minBand = minRow / TileHeight; + int maxBand = maxRow / TileHeight; + for (int b = minBand; b <= maxBand; b++) { - IMemoryOwner newOwner = allocator.Allocate(edgeSpan.Length * 2); - edgeSpan.CopyTo(newOwner.Memory.Span); - currentOwner.Dispose(); - currentOwner = newOwner; - edgeSpan = currentOwner.Memory.Span; + bandCounts[b]++; } - edgeSpan[validEdgeCount] = new GpuEdge - { - X0 = x0, - Y0 = y0, - X1 = x1, - Y1 = y1, - MinRow = minRow, - MaxRow = maxRow, - }; - bandOverlaps += (maxRow / TileHeight) - (minRow / TileHeight) + 1; - validEdgeCount++; + totalSubEdges += maxBand - minBand + 1; } } - edgeCount = validEdgeCount; - totalBandOverlaps = bandOverlaps; - - if (validEdgeCount == 0) + if (totalSubEdges == 0) { - currentOwner.Dispose(); return true; } - // Build CSR offsets and indices directly from struct fields. - Span edges = currentOwner.Memory.Span; - int bandCount = (int)DivideRoundUp(height, TileHeight); + // Prefix sum → band offsets. uint[] offsets = new uint[bandCount + 1]; - - // Count edges per band. - for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) - { - ref GpuEdge edge = ref edges[edgeIdx]; - int minBand = edge.MinRow / TileHeight; - int maxBand = edge.MaxRow / TileHeight; - for (int b = minBand; b <= maxBand; b++) - { - offsets[b]++; - } - } - - // Exclusive prefix sum → offsets[i] = start index for band i. uint running = 0; - for (int b = 0; b <= bandCount; b++) + for (int b = 0; b < bandCount; b++) { - uint count = offsets[b]; offsets[b] = running; - running += count; + running += (uint)bandCounts[b]; } - // Scatter: write local edge indices into CSR index array. - uint[] indices = new uint[bandOverlaps]; + offsets[bandCount] = running; + + // Pass 2: Flatten again and scatter edges directly into the final buffer. + IMemoryOwner finalOwner = allocator.Allocate(totalSubEdges); + Span finalSpan = finalOwner.Memory.Span; uint[] writeCursors = new uint[bandCount]; - for (int edgeIdx = 0; edgeIdx < validEdgeCount; edgeIdx++) + + foreach (ISimplePath simplePath in path.Flatten()) { - ref GpuEdge edge = ref edges[edgeIdx]; - int minBand = edge.MinRow / TileHeight; - int maxBand = edge.MaxRow / TileHeight; - for (int b = minBand; b <= maxBand; b++) + ReadOnlySpan points = simplePath.Points.Span; + if (points.Length < 2) { - uint slot = offsets[b] + writeCursors[b]; - indices[slot] = (uint)edgeIdx; - writeCursors[b]++; + continue; + } + + int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1; + for (int j = 0; j < segmentCount; j++) + { + PointF p0 = points[j]; + PointF p1 = points[j + 1 == points.Length ? 0 : j + 1]; + float fx0 = (p0.X - interestX) + samplingOffsetX; + float fy0 = (p0.Y - interestY) + samplingOffsetY; + float fx1 = (p1.X - interestX) + samplingOffsetX; + float fy1 = (p1.Y - interestY) + samplingOffsetY; + + int x0 = (int)MathF.Round(fx0 * FixedOne); + int y0 = (int)MathF.Round(fy0 * FixedOne); + int x1 = (int)MathF.Round(fx1 * FixedOne); + int y1 = (int)MathF.Round(fy1 * FixedOne); + if (y0 == y1) + { + continue; + } + + int yMinFixed = Math.Min(y0, y1); + int yMaxFixed = Math.Max(y0, y1); + int minRow = Math.Max(0, yMinFixed >> FixedShift); + int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); + if (minRow > maxRow) + { + continue; + } + + GpuEdge edge = new() { X0 = x0, Y0 = y0, X1 = x1, Y1 = y1 }; + int minBand = minRow / TileHeight; + int maxBand = maxRow / TileHeight; + for (int band = minBand; band <= maxBand; band++) + { + finalSpan[(int)(offsets[band] + writeCursors[band])] = edge; + writeCursors[band]++; + } } } - csrOffsets = offsets; - csrIndices = indices; - edgeOwner = currentOwner; + edgeOwner = finalOwner; + edgeCount = totalSubEdges; + bandOffsets = offsets; return true; } @@ -842,7 +779,8 @@ public CsrConfig(uint totalEdgeCount) } /// - /// GPU edge record matching the WGSL storage buffer layout (32 bytes, sequential). + /// GPU edge record matching the WGSL storage buffer layout (16 bytes, sequential). + /// Edges are pre-split at tile-row boundaries so each edge belongs to exactly one band. /// [StructLayout(LayoutKind.Sequential)] private struct GpuEdge @@ -851,14 +789,11 @@ private struct GpuEdge public int Y0; public int X1; public int Y1; - public int MinRow; - public int MaxRow; - public uint CsrBandOffset; - public uint DefinitionEdgeStart; } /// /// Transient per-definition geometry produced during edge buffer construction. + /// Edges are pre-split at tile-row boundaries and sorted by band. /// [StructLayout(LayoutKind.Auto)] private struct DefinitionGeometry @@ -866,24 +801,18 @@ private struct DefinitionGeometry public IMemoryOwner? EdgeOwner; public int EdgeCount; public int BandCount; - public int TotalBandOverlaps; - public uint[]? CsrOffsets; - public uint[]? CsrIndices; + public uint[]? BandOffsets; public DefinitionGeometry( IMemoryOwner? edgeOwner, int edgeCount, int bandCount, - int totalBandOverlaps, - uint[]? csrOffsets, - uint[]? csrIndices) + uint[]? bandOffsets) { this.EdgeOwner = edgeOwner; this.EdgeCount = edgeCount; this.BandCount = bandCount; - this.TotalBandOverlaps = totalBandOverlaps; - this.CsrOffsets = csrOffsets; - this.CsrIndices = csrIndices; + this.BandOffsets = bandOffsets; } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 1addddd05..86b083752 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -497,12 +497,9 @@ private bool TryRenderPreparedFlush( out nuint edgeBufferSize, out EdgePlacement[] edgePlacements, out int totalEdgeCount, - out int totalCsrEntries, - out int totalCsrIndices, - out WgpuBuffer* csrOffsetsBuffer, - out nuint csrOffsetsBufferSize, - out WgpuBuffer* csrIndicesBuffer, - out nuint csrIndicesBufferSize, + out int totalBandOffsetEntries, + out WgpuBuffer* bandOffsetsBuffer, + out nuint bandOffsetsBufferSize, out error)) { return false; @@ -524,10 +521,8 @@ private bool TryRenderPreparedFlush( edgePlacements, edgeBuffer, edgeBufferSize, - csrOffsetsBuffer, - csrOffsetsBufferSize, - csrIndicesBuffer, - csrIndicesBufferSize, + bandOffsetsBuffer, + bandOffsetsBufferSize, out error)) { return false; @@ -574,10 +569,8 @@ private bool TryDispatchPreparedCompositeCommands( EdgePlacement[] edgePlacements, WgpuBuffer* edgeBuffer, nuint edgeBufferSize, - WgpuBuffer* csrOffsetsBuffer, - nuint csrOffsetsBufferSize, - WgpuBuffer* csrIndicesBuffer, - nuint csrIndicesBufferSize, + WgpuBuffer* bandOffsetsBuffer, + nuint bandOffsetsBufferSize, out string? error) where TPixel : unmanaged, IPixel { @@ -805,14 +798,9 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out &dispatchConfig, dispatchConfigSize); - // CSR offsets and indices are pre-computed on CPU and uploaded directly. - // This eliminates the 5-dispatch GPU CSR pipeline (count → prefix-local → - // prefix-block-scan → prefix-propagate → scatter). - nuint csrOffsetsByteCount = csrOffsetsBufferSize; - nuint csrIndicesByteCount = csrIndicesBufferSize; - - // Fine composite dispatch with CSR buffers. - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; + // Band offsets are pre-computed on CPU and uploaded directly. + // Edges are pre-split at band boundaries, eliminating CSR index indirection. + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[7]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -852,22 +840,15 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out bindGroupEntries[6] = new BindGroupEntry { Binding = 6, - Buffer = csrOffsetsBuffer, - Offset = 0, - Size = csrOffsetsByteCount - }; - bindGroupEntries[7] = new BindGroupEntry - { - Binding = 7, - Buffer = csrIndicesBuffer, + Buffer = bandOffsetsBuffer, Offset = 0, - Size = csrIndicesByteCount + Size = bandOffsetsBufferSize }; BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 8, + EntryCount = 7, Entries = bindGroupEntries }; @@ -990,7 +971,7 @@ private static bool TryCreateCompositeBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1068,21 +1049,10 @@ private static bool TryCreateCompositeBindGroupLayout( MinBindingSize = 0 } }; - entries[7] = new BindGroupLayoutEntry - { - Binding = 7, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout - { - Type = BufferBindingType.ReadOnlyStorage, - HasDynamicOffset = false, - MinBindingSize = 0 - } - }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 8, + EntryCount = 7, Entries = entries }; diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index be5d9ae03..f53eda80a 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -33,10 +33,6 @@ internal static class DefaultRasterizer // Higher counts increased scheduling overhead for medium geometry workloads. private const int MaxParallelWorkerCount = 12; - // Minimum number of edge indices in a bucket before sorting for cache locality. - // Below this threshold the sort overhead exceeds the benefit of sequential edge access. - private const int EdgeIndexSortThreshold = 32; - private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private static readonly int WordBitCount = nint.Size * 8; @@ -247,22 +243,22 @@ private static void RasterizeSequentialBands( return; } - if (!TryBuildEdgeBuckets( + if (!TryBuildBandSortedEdges( edges, bandCount, bandHeight, allocator, - out IMemoryOwner bandOffsetsOwner, - out IMemoryOwner bandEdgeReferencesOwner)) + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner bandOffsetsOwner)) { ThrowInterestBoundsTooLarge(); } + using (sortedEdgesOwner) using (bandOffsetsOwner) - using (bandEdgeReferencesOwner) { + Span sortedEdges = sortedEdgesOwner.Memory.Span; Span bandOffsets = bandOffsetsOwner.Memory.Span; - Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; // Reuse the caller-provided scratch when dimensions match; create a new one otherwise. if (reusableScratch == null || !reusableScratch.CanReuse(wordsPerRow, coverStrideInt, width, bandHeight)) @@ -285,15 +281,7 @@ private static void RasterizeSequentialBands( } Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); - Span bandEdges = bandEdgeReferences.Slice(start, length); - if (length >= EdgeIndexSortThreshold) - { - // Sorting edge indices into ascending order improves cache locality when - // accessing the shared edges array: sequential indices → sequential reads. - bandEdges.Sort(); - } - - context.RasterizeEdgeTable(edges, bandEdges, bandTop); + context.RasterizeEdgeTable(sortedEdges.Slice(start, length), bandTop); context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); context.ResetTouchedRows(); } @@ -367,22 +355,22 @@ private static bool TryRasterizeParallel( return false; } - if (!TryBuildEdgeBuckets( + if (!TryBuildBandSortedEdges( edgeMemory.Span[..edgeCount], tileCount, tileHeight, allocator, - out IMemoryOwner tileOffsetsOwner, - out IMemoryOwner tileEdgeReferencesOwner)) + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner tileOffsetsOwner)) { return false; } + using (sortedEdgesOwner) using (tileOffsetsOwner) - using (tileEdgeReferencesOwner) { + Memory sortedEdgesMemory = sortedEdgesOwner.Memory; Memory tileOffsetsMemory = tileOffsetsOwner.Memory; - Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; ParallelOptions parallelOptions = new() { @@ -402,22 +390,15 @@ private static bool TryRasterizeParallel( int bandTop = tile * tileHeight; try { - ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; Span tileOffsets = tileOffsetsMemory.Span; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; int bandHeight = Math.Min(tileHeight, height - bandTop); int start = tileOffsets[tile]; int length = tileOffsets[tile + 1] - start; if (length > 0) { - Span tileEdges = tileEdgeReferences.Slice(start, length); - if (length >= EdgeIndexSortThreshold) - { - tileEdges.Sort(); - } - + ReadOnlySpan tileEdges = sortedEdgesMemory.Span.Slice(start, length); context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, tileEdges, bandTop); + context.RasterizeEdgeTable(tileEdges, bandTop); hasCoverage = true; context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); } @@ -487,44 +468,32 @@ private static void RasterizeSingleTileDirect( } /// - /// Builds a CSR (Compressed Sparse Row) edge-to-bucket index mapping. - /// Each edge is recorded in every bucket whose row range it overlaps. + /// Builds a band-sorted edge buffer where edges are duplicated into each band they touch. + /// Band offsets provide direct indexing — no per-band edge index array is needed. /// - /// The prebuilt edge table. - /// Total number of buckets. - /// Rows per bucket. - /// Temporary buffer allocator. - /// - /// On success, receives an owned buffer of length + 1 containing - /// the CSR row offsets (prefix sums). Caller is responsible for disposal. - /// - /// - /// On success, receives an owned buffer containing edge index references. Caller is responsible for disposal. - /// - /// - /// on success; - /// if the total reference count would overflow . - /// - private static bool TryBuildEdgeBuckets( + private static bool TryBuildBandSortedEdges( ReadOnlySpan edges, int bucketCount, int bucketHeight, MemoryAllocator allocator, - out IMemoryOwner offsetsOwner, - out IMemoryOwner referencesOwner) + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner offsetsOwner) { using IMemoryOwner countsOwner = allocator.Allocate(bucketCount, AllocationOptions.Clean); Span counts = countsOwner.Memory.Span; long totalRefs = 0; for (int i = 0; i < edges.Length; i++) { - int startBucket = edges[i].MinRow / bucketHeight; - int endBucket = edges[i].MaxRow / bucketHeight; + ref readonly EdgeData edge = ref edges[i]; + int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; + int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; + int startBucket = minRow / bucketHeight; + int endBucket = maxRow / bucketHeight; totalRefs += (endBucket - startBucket) + 1; if (totalRefs > int.MaxValue) { + sortedEdgesOwner = null!; offsetsOwner = null!; - referencesOwner = null!; return false; } @@ -534,7 +503,7 @@ private static bool TryBuildEdgeBuckets( } } - int totalReferences = (int)totalRefs; + int totalEdges = (int)totalRefs; offsetsOwner = allocator.Allocate(bucketCount + 1); Span offsets = offsetsOwner.Memory.Span; int offset = 0; @@ -549,15 +518,18 @@ private static bool TryBuildEdgeBuckets( Span writeCursor = writeCursorOwner.Memory.Span; offsets[..bucketCount].CopyTo(writeCursor); - referencesOwner = allocator.Allocate(totalReferences); - Span references = referencesOwner.Memory.Span; - for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) + sortedEdgesOwner = allocator.Allocate(totalEdges); + Span sorted = sortedEdgesOwner.Memory.Span; + for (int i = 0; i < edges.Length; i++) { - int startBucket = edges[edgeIndex].MinRow / bucketHeight; - int endBucket = edges[edgeIndex].MaxRow / bucketHeight; + ref readonly EdgeData edge = ref edges[i]; + int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; + int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; + int startBucket = minRow / bucketHeight; + int endBucket = maxRow / bucketHeight; for (int b = startBucket; b <= endBucket; b++) { - references[writeCursor[b]++] = edgeIndex; + sorted[writeCursor[b]++] = edge; } } @@ -617,8 +589,7 @@ private static int BuildEdgeTable( continue; } - ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow); - destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); + destination[count++] = new EdgeData(fx0, fy0, fx1, fy1); } } @@ -677,33 +648,6 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); - /// - /// Computes the inclusive row range affected by a clipped non-horizontal edge. - /// - /// Edge start Y in 24.8 fixed-point. - /// Edge end Y in 24.8 fixed-point. - /// First affected integer scan row. - /// Last affected integer scan row. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ComputeEdgeRowBounds(int y0, int y1, out int minRow, out int maxRow) - { - int y0Row = y0 >> FixedShift; - int y1Row = y1 >> FixedShift; - - // First touched row is floor(min(y0, y1)). - minRow = y0Row < y1Row ? y0Row : y1Row; - - int y0Fraction = y0 & (FixedOne - 1); - int y1Fraction = y1 & (FixedOne - 1); - - // Last touched row is ceil(max(y)) - 1: - // - when fractional part is non-zero, row is unchanged; - // - when exactly on a row boundary, subtract 1 (edge ownership rule). - int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1); - int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1); - maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate; - } - /// /// Clips a fixed-point segment against vertical bounds. /// @@ -1029,50 +973,9 @@ public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) int y1 = edge.Y1; // Fast-path: edge is fully within this band — no clipping needed. - // MinRow >= bandTop guarantees min(y0,y1) >= bandTopFixed. - // MaxRow < bandTop + height guarantees max(y0,y1) < bandBottomFixed. - if (edge.MinRow >= bandTop && edge.MaxRow < bandTop + this.height) - { - this.RasterizeLine(x0, y0 - bandTopFixed, x1, y1 - bandTopFixed); - continue; - } - - if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) - { - continue; - } - - // Convert global scanner Y to band-local Y after clipping. - y0 -= bandTopFixed; - y1 -= bandTopFixed; - - this.RasterizeLine(x0, y0, x1, y1); - } - } - - /// - /// Rasterizes a subset of prebuilt edges that intersect this context's vertical range. - /// - /// Shared edge table. - /// Indices into for this band/tile. - /// Top row of this context in global scanner-local coordinates. - public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan edgeIndices, int bandTop) - { - int bandTopFixed = bandTop * FixedOne; - int bandBottomFixed = bandTopFixed + (this.height * FixedOne); - - for (int i = 0; i < edgeIndices.Length; i++) - { - EdgeData edge = edges[edgeIndices[i]]; - int x0 = edge.X0; - int y0 = edge.Y0; - int x1 = edge.X1; - int y1 = edge.Y1; - - // Fast-path: edge is fully within this band — no clipping needed. - // MinRow >= bandTop guarantees min(y0,y1) >= bandTopFixed. - // MaxRow < bandTop + height guarantees max(y0,y1) < bandBottomFixed. - if (edge.MinRow >= bandTop && edge.MaxRow < bandTop + this.height) + int minY = Math.Min(y0, y1); + int maxY = Math.Max(y0, y1); + if (minY >= bandTopFixed && maxY <= bandBottomFixed) { this.RasterizeLine(x0, y0 - bandTopFixed, x1, y1 - bandTopFixed); continue; @@ -2111,11 +2014,12 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) } /// - /// Immutable scanner-local edge record with precomputed affected-row bounds. + /// Immutable scanner-local edge record (16 bytes). /// /// /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path - /// access without per-read unpacking. + /// access without per-read unpacking. Row bounds are computed inline from Y coordinates + /// where needed. /// internal readonly struct EdgeData { @@ -2139,27 +2043,15 @@ internal readonly struct EdgeData /// public readonly int Y1; - /// - /// Gets the first scanner row affected by this edge. - /// - public readonly int MinRow; - - /// - /// Gets the last scanner row affected by this edge. - /// - public readonly int MaxRow; - /// /// Initializes a new instance of the struct. /// - public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) + public EdgeData(int x0, int y0, int x1, int y1) { this.X0 = x0; this.Y0 = y0; this.X1 = x1; this.Y1 = y1; - this.MinRow = minRow; - this.MaxRow = maxRow; } } From 34f110f76b5d3cca6b03b0fe7139d3efcbf9557a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 22:31:02 +1000 Subject: [PATCH 094/136] Add small-geometry fast path to rasterizer --- .../Processing/Backends/DefaultRasterizer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index f53eda80a..2424e2234 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -330,10 +330,11 @@ private static bool TryRasterizeParallel( } int tileCount = (height + tileHeight - 1) / tileHeight; - if (tileCount == 1) + if (tileCount == 1 || edgeCount <= 64) { - // Tiny workload fast path: avoid bucket construction and worker scheduling - // when everything fits in a single tile. + // Small-geometry fast path: for paths with few edges (e.g. a stroked line + // producing ~6-10 edges), iterating all edges against all rows is far cheaper + // than the allocation overhead of band sorting + Parallel.For scheduling. RasterizeSingleTileDirect( edgeMemory.Span[..edgeCount], width, From 1229e736b429b4a7f5d80b95b0e1bfab85a98dce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 22:57:06 +1000 Subject: [PATCH 095/136] Update reference images --- src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs | 9 ++++++++- .../Clear_WithClipPath_MatchesReference_Rgba32.png | 2 +- ...teRegion_LocalCoordinates_MatchesReference_Rgba32.png | 4 ++-- ...dRegionsAndStateIsolation_MatchesReference_Rgba32.png | 4 ++-- ...jiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png | 2 +- ...EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png | 4 ++-- .../DrawPrimitiveHelpers_MatchesReference_Rgba32.png | 4 ++-- ...wText_AlongPathWithOrigin_MatchesReference_Rgba32.png | 4 ++-- .../DrawText_FillAndStroke_MatchesReference_Rgba32.png | 4 ++-- ...ine_WithLineMetricsGuides_MatchesReference_Rgba32.png | 4 ++-- .../DrawText_PenOnly_MatchesReference_Rgba32.png | 4 ++-- ...ngAlignmentAndLineSpacing_MatchesReference_Rgba32.png | 4 ++-- ...Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png | 4 ++-- .../Draw_PathBuilder_MatchesReference_Rgba32.png | 4 ++-- ...ithPatternAndGradientPens_MatchesReference_Rgba32.png | 4 ++-- .../Fill_PathBuilder_MatchesReference_Rgba32.png | 4 ++-- ...tingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png | 4 ++-- ...GradientAndPatternBrushes_MatchesReference_Rgba32.png | 2 +- ...me_WithReadbackCapability_MatchesReference_Rgba32.png | 4 ++-- .../Process_Path_MatchesReference_Rgba32.png | 4 ++-- .../RestoreTo_MultipleStates_MatchesReference_Rgba32.png | 4 ++-- ...gPathHorizontal_Rgba32_Blank120x120_type-triangle.png | 4 ++-- ...ongPathVertical_Rgba32_Blank250x250_type-triangle.png | 2 +- .../CanDrawTextVertical2_Rgba32_Blank48x935.png | 2 +- .../CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png | 4 ++-- .../CanDrawTextVertical_Rgba32_Blank500x400.png | 2 +- .../CanFillTextVertical_Rgba32_Blank500x400.png | 2 +- .../CanRenderTextOutOfBoundsIssue301.png | 4 ++-- ...55,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png | 4 ++-- ...55,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png | 4 ++-- ..._F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 4 ++-- ..._F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 4 ++-- .../Clear_AlwaysOverridesPreviousColor_Blue.png | 3 +++ .../Clear_AlwaysOverridesPreviousColor_Khaki.png | 3 +++ .../Clear_DoesNotDependOnSinglePixelType_Argb32.png | 3 +++ .../Clear_DoesNotDependOnSinglePixelType_Rgba32.png | 3 +++ .../Clear_DoesNotDependOnSinglePixelType_RgbaVector.png | 3 +++ .../Clear_DoesNotDependOnSize_Blank16x7.png | 3 +++ .../Clear_DoesNotDependOnSize_Blank1x1.png | 3 +++ .../Clear_DoesNotDependOnSize_Blank33x32.png | 3 +++ .../Clear_DoesNotDependOnSize_Blank400x500.png | 3 +++ .../Clear_DoesNotDependOnSize_Blank7x4.png | 3 +++ ...ear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | 3 +++ ...ear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | 3 +++ ...emoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | 3 +++ ...emoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | 3 +++ ...lear_WhenColorIsOpaque_OverridePreviousColor_Blue.png | 3 +++ ...ear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png | 3 +++ .../ClipOffset_offset_x-20_y-100.png | 4 ++-- .../ClipOffset_offset_x-20_y-20.png | 4 ++-- .../ClipOffset_offset_x0_y0.png | 2 +- .../ClipOffset_offset_x20_y20.png | 4 ++-- .../ClipOffset_offset_x40_y60.png | 4 ++-- .../DrawBeziers_HotPink_A255_T5.png | 4 ++-- .../DrawBeziers_Red_A255_T3.png | 4 ++-- .../DrawBeziers_White_A255_T1.5.png | 4 ++-- .../DrawBeziers_White_A255_T15.png | 4 ++-- .../ProcessWithDrawingCanvasTests/DrawComplexPolygon.png | 4 ++-- .../DrawComplexPolygon__Overlap.png | 4 ++-- .../DrawComplexPolygon__Transparent.png | 4 ++-- .../DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png | 4 ++-- .../DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png | 4 ++-- ...rawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- ...rawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- ...awLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- .../DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png | 4 ++-- .../DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png | 4 ++-- .../DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png | 4 ++-- .../DrawPathClippedOnTop.png | 4 ++-- ...DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png | 4 ++-- .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png | 2 +- ...olygon_Transformed_Rgba32_BasicTestPattern250x350.png | 4 ++-- ...ic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png | 4 ++-- ...ic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png | 4 ++-- ...w_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png | 4 ++-- ...w_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png | 4 ++-- ...wRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png | 4 ++-- ...wRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png | 2 +- ...276x336_(255,255,255,255)_ColorFontsEnabled-False.png | 4 ++-- ...1276x336_(255,255,255,255)_ColorFontsEnabled-True.png | 4 ++-- ...ntRendering_Rgba32_Solid400x200_(255,255,255,255).png | 2 +- .../FillComplexPolygon_SolidFill.png | 4 ++-- .../FillComplexPolygon_SolidFill__Overlap.png | 4 ++-- .../FillComplexPolygon_SolidFill__Transparent.png | 4 ++-- ...tBrushAxisParallelEllipsesWithDifferentRatio_0.40.png | 4 ++-- ...shRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png | 4 ++-- ...shRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png | 4 ++-- .../FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png | 4 ++-- ...dientBrushDiagonalReturnsCorrectImages_BottomLeft.png | 4 ++-- ...ientBrushDiagonalReturnsCorrectImages_BottomRight.png | 4 ++-- ...GradientBrushDiagonalReturnsCorrectImages_TopLeft.png | 4 ++-- ...radientBrushDiagonalReturnsCorrectImages_TopRight.png | 4 ++-- ...adientBrushHorizontalGradientWithRepMode_DontFill.png | 4 ++-- ...FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png | 4 ++-- ...oundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png | 4 ++-- ...oundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png | 4 ++-- ...eBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png | 4 ++-- ...deBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png | 4 ++-- .../ProcessWithDrawingCanvasTests/FillPathSVGArcs.png | 4 ++-- .../FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png | 4 ++-- .../FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png | 4 ++-- .../FillPolygon_ImageBrush_Rgba32_Car.png | 4 ++-- .../FillPolygon_ImageBrush_Rgba32_ducky.png | 4 ++-- .../FillPolygon_Pattern_Rgba32.png | 4 ++-- .../FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png | 4 ++-- .../FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png | 4 ++-- .../FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png | 4 ++-- .../FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png | 4 ++-- .../FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png | 4 ++-- .../FillPolygon_Solid_Bgr24_Yellow_A1.png | 4 ++-- .../FillPolygon_Solid_Rgba32_White_A0.6.png | 4 ++-- .../FillPolygon_Solid_Rgba32_White_A1.png | 4 ++-- ..._Solid_Transformed_Rgba32_BasicTestPattern250x350.png | 4 ++-- .../FillPolygon_StarCircle_AllOperations_Difference.png | 4 ++-- ...FillPolygon_StarCircle_AllOperations_Intersection.png | 4 ++-- .../FillPolygon_StarCircle_AllOperations_Union.png | 4 ++-- .../FillPolygon_StarCircle_AllOperations_Xor.png | 4 ++-- ...-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png | 3 +++ ..._alpha-0.5_blenderMode-Multiply_blendPercentage-1.png | 3 +++ ...ue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png | 3 +++ ...-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png | 3 +++ ..._alpha-1_blenderMode-Multiply_blendPercentage-0.5.png | 3 +++ ...ue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png | 3 +++ ...een_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png | 3 +++ ...lpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png | 3 +++ ..._alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png | 3 +++ ...ink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png | 3 +++ ...lpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png | 3 +++ ..._alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png | 3 +++ ...-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png | 3 +++ ..._alpha-0.5_blenderMode-Multiply_blendPercentage-1.png | 3 +++ ...ue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png | 3 +++ ...-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png | 3 +++ ..._alpha-1_blenderMode-Multiply_blendPercentage-0.5.png | 3 +++ ...ue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png | 3 +++ ...een_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png | 3 +++ ...lpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png | 3 +++ ..._alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png | 3 +++ ...ink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png | 3 +++ ...lpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png | 3 +++ ..._alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png | 3 +++ ...lSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png | 3 +++ ...lSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png | 3 +++ ...idBrush_DoesNotDependOnSinglePixelType_RgbaVector.png | 3 +++ .../FillSolidBrush_DoesNotDependOnSize_Blank16x7.png | 3 +++ .../FillSolidBrush_DoesNotDependOnSize_Blank1x1.png | 3 +++ .../FillSolidBrush_DoesNotDependOnSize_Blank33x32.png | 3 +++ .../FillSolidBrush_DoesNotDependOnSize_Blank400x500.png | 3 +++ .../FillSolidBrush_DoesNotDependOnSize_Blank7x4.png | 3 +++ ...age_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png | 3 +++ ...age_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png | 3 +++ ...rush_WhenColorIsOpaque_OverridePreviousColor_Blue.png | 3 +++ ...ush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png | 3 +++ ...55,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png | 2 +- ...255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 2 +- ...5,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 2 +- ...55,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...5,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- .../FontShapesAreRenderedCorrectly_LargeText.png | 4 ++-- ...55,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...5,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- ...(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png | 4 ++-- ...ixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png | 4 ++-- ...(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png | 4 ++-- ...penSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png | 4 ++-- ...LaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png | 2 +- ...enSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png | 2 +- ..._F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png | 4 ++-- .../LargeGeoJson_Mississippi_LinesScaled_Scale(10).png | 4 ++-- .../LargeGeoJson_States_Fill.png | 4 ++-- ...wingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png | 2 +- ...PathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png | 2 +- .../SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png | 4 ++-- .../TextPositioningIsRobust_OpenSans-Regular.ttf.png | 4 ++-- ...yphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png | 3 +++ ...dReleasesPreparedCoverage_DefaultBackend_DrawText.png | 3 +++ ...sAndReleasesPreparedCoverage_SkiaBackend_DrawText.png | 3 +++ ...roke_MatchesDefaultOutput_DrawPath_Stroke_Default.png | 4 ++-- ...hesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png | 4 ++-- ...efaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png | 4 ++-- ...esBlendFastPath_RepeatedGlyphs_AfterClear_Default.png | 4 ++-- ...stPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png | 4 ++-- ...th_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png | 4 ++-- ...edGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png | 4 ++-- ...UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png | 4 ++-- ...CoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png | 4 ++-- ...ndersAndReleasesPreparedCoverage_DrawText_Default.png | 4 ++-- ...Output_FillPath_MultipleSeparate_WebGPU_CPURegion.png | 4 ++-- ...ut_FillPath_MultipleSeparate_WebGPU_NativeSurface.png | 4 ++-- ...tions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png | 4 ++-- ...s_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png | 4 ++-- ...Options_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png | 4 ++-- ...ons_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png | 4 ++-- ...ptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png | 4 ++-- ...ns_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png | 4 ++-- ...ions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png | 4 ++-- ..._ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png | 4 ++-- ...Options_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png | 4 ++-- ...ons_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png | 4 ++-- ...ptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png | 4 ++-- ...ns_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png | 4 ++-- ...ath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png | 4 ++-- ...NativeSurfaceSubregionParity_WebGPU_NativeSurface.png | 4 ++-- ...put_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png | 4 ++-- ...FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png | 4 ++-- ...ebGPUBackend_MatchesDefaultOutput_Process_Default.png | 4 ++-- ...Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png | 4 ++-- ...Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png | 4 ++-- ...y_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png | 4 ++-- ...y_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png | 4 ++-- ...nly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png | 4 ++-- ...nly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png | 4 ++-- ...xtOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png | 4 ++-- ...Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png | 4 ++-- ...Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png | 2 +- ...nt_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png | 4 ++-- ...nt_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png | 4 ++-- 222 files changed, 486 insertions(+), 314 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png create mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png create mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png create mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs index 4c8f78d66..edd082e2a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -6,12 +6,19 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Native WebGPU surface capability attached to . /// +/// +/// The handle must remain valid for the lifetime of any +/// that processes frames using this capability. +/// The backend caches per-device GPU resources (pipelines, buffers) that reference +/// the device internally. Ensure the device is not released while any backend +/// instance may still reference it. +/// public sealed class WebGPUSurfaceCapability { /// /// Initializes a new instance of the class. /// - /// Opaque WGPUDevice* handle. + /// Opaque WGPUDevice* handle. Must remain valid for the lifetime of any backend that uses this capability. /// Opaque WGPUQueue* handle. /// Opaque WGPUTexture* handle for the current frame when writable upload is supported. /// Opaque WGPUTextureView* handle for the current frame. diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png index b24c34725..9bc8bbcc6 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d28b153b714f7097f51f269295fa3e625be5958f117ce5017ac602a94ab8cbb2 +oid sha256:b4bca6efabaacd96d852a06a62a3e966c589463cd4e977923e39101461e4f825 size 10930 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png index 33d3f8ef0..8747b0439 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41d684c2a171f8a0f633a00e4eb960458af2daf5ad981cda63fafbfa1c6c88d9 -size 2114 +oid sha256:764948d06820b75dfcbf9341fb5b30c3b5a043022dce2932214d3b83b42d2718 +size 2070 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png index efb2a587d..fab753ae5 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:900a4c73a62edb0df9c11cfe1ab81d55532e175d81148f3682cc5c38e1ea46f9 -size 12352 +oid sha256:f2a4cbc37c65d3d58ace60086f4eb80b0bf683313db280e820d394bef6f43e38 +size 13870 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png index 9433427aa..f174b8178 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e075f71a20f3fb8957b2412820eb533715ee3968d46a6454c9713b3f0d4641f +oid sha256:0981d21ed8f75ee1fffefdae75505365bea3715bfbf8bda56d278792766f9b09 size 10939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png index 5b46fff4a..681c0543a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:084c39dc74b3cc84d16b057e785fa6576a09bac3aee87437b3de10c7b4f99fd4 -size 10939 +oid sha256:53206cc3329cb5a49afd48cb17a98a4ced8b38bc6f0b90b5f02e647b0f23e8ee +size 10940 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png index 08a73286d..ddd97673e 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b422cdaad5f46da741f9516d93425653f4ad28c34a3741f15194cc7b45298f56 -size 9137 +oid sha256:f1b8f27b7844739e6fc64e0e63e5dec5e93f72bec2e9c5f11d7f9ccda0788488 +size 9134 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png index b75804d8b..d636d4bf2 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641d8788a235efcca45ba2936e04cf8efd0440704b308f0a0b2e094d9aff59dc -size 11044 +oid sha256:a95282d25ea59eac6d996f574b7f1167c50f2dffeef719594f2cf84bbef17652 +size 11049 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png index 5c4c1774b..69d2f6f0d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0fa52d4c98829843624056b400d33b3c0e61101ef441eadabf78d76e0c02b13 -size 21210 +oid sha256:ab9520ac162d3a40a8b96441c4fe5e83a3b8142d89f17ec41161a37afa30aad7 +size 21366 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png index 8b6f68e53..471d4af04 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66ffc518934398824ef7762397afc0dea7d7ae227f041c6f560c5da7e76e0d51 -size 26117 +oid sha256:6e52723224dda43093a5e1f1330abe9fed214c5308772278b4c3791d7bd0f2a0 +size 26116 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png index e7a5d8d13..71c0cc3f2 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eb7f23ea51edd63746d319b68fc3608b4894af394c0328761914c3838efd38f -size 3198 +oid sha256:f1d471409c0c0eae66ea5f92bfef64da1de8808f132b4746282874d9b2740c7c +size 3203 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png index d50eac278..baafc25f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a14fce97ebad01b5a5978ef1f475387407357d299a3e641494e03351babb302c -size 45437 +oid sha256:2ace94fdd7f0a55cb5427f879c1ce8fdd7693a594e50c6f720df7611dfb28f78 +size 45434 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png index d3e5b6e7a..0428bd7d6 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f021ad48a06ffd4d107bae867746e299561607425762a8402c871c83fdb8f968 -size 3836 +oid sha256:cb3a3b312deb41f7faf1b6ce06538496903c1be02672400ba760e8a0eca7171f +size 3653 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png index 701efcbf9..8635613c1 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:250654dfafbfe77e1bde33eef9cc9ed15d1c482aaf6794ba868eeb3b13c4587a -size 3458 +oid sha256:5823d96d7d2eaba23b23c716c0337000c98baa35b38aa67fd35cfe0a9f00dc94 +size 3831 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png index 05629b290..b370b2fe4 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4d703eefd1e0c88bc6bd952382260179f513d2f9c65c9b0ef25943ae8d1e6b2 -size 11158 +oid sha256:260ec3704eeed9f6d005bfd3475fe5b812a253543bd9726b8e8c7e562b3b73c8 +size 13277 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png index 9d052627a..e3b66f93d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85c01d37cb482890dc5049408be8150ec5f6b64cf818fb138cd6332d0d714473 -size 2711 +oid sha256:026cd69f6f77bec21f60fa76091a1afa6278beedf0a360288bb92269beaba113 +size 2682 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png index d8416d670..9b70ed9c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53747387140e5016d3369aa46400c4214c99b2368421d1023bebb1dc8479b031 -size 8267 +oid sha256:3940441662db9d40c73733bb7c3f54be823dd4a12a1458bc24ac4b0fc05d01ed +size 8271 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png index 9aa616d5c..88e1bfb77 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:630f6e24deb0eb71d222a9f5f588ffd70cb01e3343cc7172e72ee566b054d7b7 +oid sha256:fc897cff98e55d62d410b225fe585bd4a6e8ef28d4b8bb08b71b498470cbcab7 size 18965 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png index 096f34c82..36b58fc84 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e -size 12939 +oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 +size 12961 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png index 096f34c82..36b58fc84 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e -size 12939 +oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 +size 12961 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png index 5787f6dcb..89eb299d0 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1685e8d64846f15e8f88f2e9e3e82628f8c08792e4b3beff32b566b6dccca8ba -size 4875 +oid sha256:159624425a1310a73dcef13263440546f2778c0bdc1871d8f36ae8bda7325907 +size 4889 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 1ab954d5d..a187c316a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70beac5ff86d52b20e44dc6426747949d8308fb756397f305fd50de303e0cd1b -size 4387 +oid sha256:d9ef4be7abbe91ecbdbdc83345fc2622c904291f4df3c260cf4d4ddc78cf48fe +size 4379 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png index 9e50b2fa0..2cfa75908 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd551e861f821dd70f9a915957703b8c691ccf30a71159e32ff6d301c4c1a4fe +oid sha256:61c21eaea8f0bb4676954df6576aa7cf70a398297b92ac9e247883347d08263a size 5181 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png index 9db4dd1db..8801715ee 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d78831cd59a95bea191c986ec931d251e6e7243b393a759239c43b632443267a +oid sha256:f54f5715132c2f81447205912f6c199c1dee9a2356fc402d5ee14e22d6f2a392 size 4988 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png index 483091b77..8988f8248 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d3593b23fc0f52360731271313e444175efbbe5a3fe9df0e01422bb66cd311d -size 4906 +oid sha256:f6b0d58b734e6bb0ee199cdcfe00332ffead6a0cc5fd2eebb9a835a11a41a937 +size 4903 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png index 62efbae2d..350519917 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0c0f7ebf2bbb452f8e93691ff62316a116f92aa7a7e8eb0190d277a8130ec99 +oid sha256:bae265a8c3aaeaeb034bb549f3bca384c5dbfd90725ca2f2ad3151d531bc5dfc size 13195 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png index 5a7d0917a..0ff4d859b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bb4baf2bde0ef826e7723c10682c382ddf0919d79c37a3645131e609a65e586 +oid sha256:fada233e5e7d359c3ba84e6c1debd375ddbc46e4b1e5111c0456bd0ee28526cd size 4482 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png index 2d7907dad..20845d0e6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2438c3dc6c663a4e51f6c33370a79c0e1a5a659a0508ff2c3696838a183da19e -size 1133 +oid sha256:6d81fbec8890cdd62f4cf4ed5df3742441468d7df3ac3555435ef9f6df287000 +size 1134 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png index 335809eec..aedd4872d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d906e2161a7e83a02fe103d37f7502f6364666f848963119f16e968ebaccaa59 -size 1960 +oid sha256:02581fcceb6031debb560705641cb9ee3d0ea9c1cff72f5fe0260092637b59c8 +size 1957 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png index 2b116b146..820d13df3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5209d55719175ad95aa4af0ee7b91404c1f0870b0bbf5633d9b6a5041901a88 -size 1723 +oid sha256:71cdeeab2372d7f1ed09f98fe2103045637111f2b807a41905bf1f197fc85e86 +size 1716 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png index 6489d53c3..27d3ab6d4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffd79c62b337bc1df02c3f243f63553bd9efce838a4a8f110995f943124dcefa -size 2591 +oid sha256:bab6a51f70ce450693978a78a94488978f13c0f4e36ef5bda10c21fd59c4d108 +size 2589 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png index a8cc5540f..6bdb2b7a6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3421a0f879544e215c4266b111596c83868168872e10303534761c455bd03b12 -size 2501 +oid sha256:127377f85a93c6b8d4f3836248e89eefad399d39abe328d85693b4e0997eb8ff +size 2500 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png new file mode 100644 index 000000000..5254df120 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bbd2c59e95ae401a038f69d9de433b56ea89c493bf5d73af54197fadf032393 +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png new file mode 100644 index 000000000..4bd589703 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27281a193533244c31acb485c7ecacc047b505042a1bbfd3c214016db2b6f7b1 +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png new file mode 100644 index 000000000..113c9e069 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577cff471034b801e84c2df271946e59e441d0890910b949dcd7b81b25f38d58 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png new file mode 100644 index 000000000..d406a3275 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8df185b0b10595bba92b871646a6b349b308221e63c2ead096e718676716bddd +size 72 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png new file mode 100644 index 000000000..4c6092dfc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e7483e76c3b65b94b68499f93f07b2c73435353adf46638c2d1fe16d62f6a0 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png new file mode 100644 index 000000000..af764cb13 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2eeed8c081a355f23062a56ea80f1891745c63f8468d65771e49418b508cd6 +size 119 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png new file mode 100644 index 000000000..0a126de11 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c24b47a3afd5d7185d9722cd8f0bd4274489bedd4e09c8d23a66e49af405dc2 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png new file mode 100644 index 000000000..8570310b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c70c09a82dfbb4db1955e417c1f24ea90178f5234bba420e71f74a094218c7c +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png new file mode 100644 index 000000000..3730d9c35 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfc380297d61413eec1b9a3f634ae0489c22fb310b004e1ecf6ea26ecc28b5f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png index 0a6844498..54b5a6bfd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ac8568a8cd6b0480b541b74a06d9a23d57ecb88a1f761ed84ac3ac01628c2e7 -size 3674 +oid sha256:13ecb101e50d82f149f2e3360277696bf3ae1c915fe26b9e52132df7630dd3d1 +size 3677 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png index 87bd10fe5..dfb8cdd0b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bb79d9722ae69e357a40793034a552976e16083d3ec70cb0d59975e1a90781c -size 5004 +oid sha256:d2aa8c26eb90c0892eb8c8cbf4094ffac251aafa42929cda34f0183485c643f4 +size 6665 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png index 3a0c39b2a..eade21ef1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5f0e9be167df587af31e95fb0738f15128a191947199fdc614be15230658862 +oid sha256:21367b243982ee878a6bcd59c4dd68f7c2b427c2ee9656b61b01b52f65ae462b size 5356 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png index 4b8e518d4..213fe08e5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53e0d07c4f930c7ada67b7648501cb7518e09724fd9605716f5231d6e6821961 -size 5401 +oid sha256:11a8d5aea1e3dc7bd44556970d08c63182387701546992460acf563101b1355d +size 5386 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png index 3d61682c4..2c0bce376 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd217c38b95baedd42064b696d975805120d91561c8d77248b749d35c1fbcf75 -size 2315 +oid sha256:fdde871848dc218f91a4535e1af8333571fa88e82aaec15112e0841277e6ac2d +size 2364 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png index 84a84ba79..5d699c129 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e531b54fbfcbcba2df2a3373734314a1644541a2faf8c15420c53a959bb57a7 -size 4613 +oid sha256:78b010bd2d394114df808155d622ae18e786de07af063942d97d4ae849b3ad83 +size 4627 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png index 6eaaee087..58142a3f5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1839a05c5eeb8c7b90758a7b9c3d2919a726a78eefd9de2728f5edf37a2018a -size 4613 +oid sha256:340eec740605d3f94801c1db0d8de14043fbcd3b2752a9edee9e60c4205c4ba5 +size 4627 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png index 8c376a110..cac478b4e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1057cf06d0acc8dd05883c14475210953827b0cf8cde751c8dc2bc8eedc6554d -size 4613 +oid sha256:d83743330f8b3d1a36929118c939af29f58c9ffa9a133b49f4e8a42ce657712a +size 4624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png index 8c376a110..cac478b4e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1057cf06d0acc8dd05883c14475210953827b0cf8cde751c8dc2bc8eedc6554d -size 4613 +oid sha256:d83743330f8b3d1a36929118c939af29f58c9ffa9a133b49f4e8a42ce657712a +size 4624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png index ba487fd74..2f18d1dcf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaa586690cc2b6f379863af5f5e8cf1566a5146d77167dd90e2b5741529cc99f -size 4499 +oid sha256:bd9d8ca37fda3431ade9c1b5bdf2c2cb77baccd33f082a3191c57685b175d568 +size 3845 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png index 7f1c0cb07..e660027ba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e86050c55b152072eba15794a2409d7a2dff176679eb44ec73baa6744da5b1d0 -size 6124 +oid sha256:4ca0865255e4b5091ede748dfd964900745cf2ac7a83f64170119f44a4b9dd2f +size 5748 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png index 9a7f7901f..1c10a0e69 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57bd54dc3d42753e9d866785d8efa8ec0a79398de26325913973b005d40cd387 -size 4139 +oid sha256:0fcdb023797fee203cafe3e184a2492e915f5d141f9149d5037b72072628071f +size 3712 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png index a2902c5a1..b3c12d506 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed1478e8fc206b1beaa69a7df49cfbb26adcb395c21bbea85657b9e647f9ef14 -size 2874 +oid sha256:4eb0493bc24df7d4dd74f02d9e7ad00074f5fd237ed05bf4a5bb36ab01588fa9 +size 2795 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png index 78098f2df..ac94cc581 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c65de578e11a666d95ba9f3f01d19d34f2c376219babcf8a7d032e2c55a43558 -size 3106 +oid sha256:f7eedeae9de91af7e3ceb21f4abde363c7becb175ac87d72506fefa7013cd708 +size 3162 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png index f81d2f0a6..e5e3de9f4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:246709450f82f00a2008cda56661d313783913a0db9a2f87741abc97dd662eb1 -size 2412 +oid sha256:3415ade15c7cdce6b0f1ddbd8a944a8dde25d107306d66ae6b3afa96844a0dfd +size 2233 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png index bba63ff53..72a2804a7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:549fec09d5fc231dfc9ac7c72f69cea66be07303216467e335434d412ceca67a -size 2511 +oid sha256:9850958e82712956717dfbaa54f30426628051b75f92461f8b8dabd5ae673177 +size 2256 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png index a9e1d1018..2e4f1c99b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d08e712955e19d82bb5a38247f1a7cbda0bc79e9dfa76514e08e0e90a89ac9 -size 2521 +oid sha256:9dd22a2fb86561ee9871b2427333d5390ee943bf52270b5ab788cefa15c3cb07 +size 4192 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png index 28e62bc17..5c4b1b9db 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95e68c3f3c108915ccc89ccbb13d4acc089aad3f7d8eff38a263c3016d233511 -size 2445 +oid sha256:2ecccb5372b5d9f4e5198589eb053f869f498825c83e1cc9625c4857b3fa45c4 +size 2189 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png index 6ae3222d4..df3d4e075 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:575650693f22358528fa2514ce474a1b50b228dff7ec00ed8c695981ade6f12e -size 2300 +oid sha256:74a6ab83344a143b2160b1996307464a7c56f6b7ef3b1de8ecd1b588eca15713 +size 2172 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png index 9d82ad2cc..d27625ceb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281d3c8349ea7e15961d1e0be5c5a0c4aad743295381f89bd3f9f7f43a02ac24 -size 2363 +oid sha256:6316a1065534532b72d9975223b9714907690b39c20e09ed65d846c7dffc2417 +size 2183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png index a37ebfa78..b63118945 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9164f2c53d94e344122f458c2c3d31f5bb1f0aae9f88dc003bb6fd07b827904 -size 209 +oid sha256:4c8a0a8dfad42d55f78144a6748634647a02c1076e5096c5ff1184c63ab8ea49 +size 206 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png index 95b8be0e8..f9bb9b729 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1fcfd5112a7e5c41d9c9ce257da4fdf5e60f76167f7a81cc6790c554b616e60 -size 5837 +oid sha256:aee03a50f20101e217cbcb7dcb227b94d52a8d09748a923e5f6f2e69c60f858f +size 5801 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png index fdcb3ce72..a857ca811 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c0bee3610654a496f70379229e4c920f2d18d9d8c9830bb1e2ad287fbb18aa7 -size 3889 +oid sha256:9a894d928cefd7c78f8c3911d31cae00a9a97aef89be64568b0a16c68b1b8a3e +size 3558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png index 2bbf451ed..d2326aaa4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8be397a2c3ea3aeee259dc407633f0bf3f6146acda86a1d7bd8e75f4ffa42b7 -size 3492 +oid sha256:4f435173db003d7ddb4da76fb94745b0ec38c519b4b46aa7e9808ba5129c4b8b +size 3405 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png index 4dbc02b7c..f6b291b02 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb78c94d064a48e523af4b950507ee0fa7158a7eaa29529dbfdc8676d4c5f35c -size 3901 +oid sha256:21daf8a4a2a072942ef46e4f7c7a572943c702eae801d45c5466f7d8a577692e +size 3537 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png index fb1965988..e9e3ae1e3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9da410ee320f2de0f95a9b37abb1d9306a19e6e6e50ad8ada02766dbcc78bc +oid sha256:54ea5ce83e31694e5d4813ae10d431bf73bd0c2c1bb3acbb6490dad2b15b74ff size 1264 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png index 05eea0f68..1995db5d3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de873a4abd145eb0aa200df4bccf7b43dcf48a97c97aaa7397b3459b23535eab -size 8823 +oid sha256:c931830d335f15f3232b0f9ad51443155dc8e7bd0bf77538414165479719cad3 +size 8839 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png index d4e7c41a5..9d1090e30 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:766844bcd409f83dd46ff5c0f2615bd9b31e3fa9719109d3127940508862715c -size 3119 +oid sha256:f63ac0247fc56eb2812847f88a0bbc0ac12634374bfcdad017ee53771e310f0b +size 3122 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png index 474f9fd69..0e9aa3923 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa3c5c1e9033618a4bef1b02427176991eb9b767b6570948b55c1067d70ff771 -size 3921 +oid sha256:2d7edc251c9e67df4dfdab4524f22ddb491f67cd085f8790c5c450ae39bfec04 +size 3916 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png index 58256c3f3..81fe22adb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff56241312753f433b55ac70ec8bc12b3f164ad24da212581b53c637cd1711fc -size 8675 +oid sha256:dc8d2eb8845cc1a7de9ed11a7cc4d8e750b372817255351b3d3c1c8fc7cd29ef +size 8671 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png index e962adb7d..7e8d52c8a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f3b08907243b9afa6ec004da2e013cfd82ded5e287e28b02b940b799aabaa2 -size 11445 +oid sha256:b2f245d2324ff1e5bff2271ae233d395667f122b306afc5d70b429ed3e456763 +size 11434 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png index 5120c4629..90dd9b2a0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58c86318d4963c1841c18c1bf5b88a661427585ef0eee6fb9825d24fc2e64820 -size 9158 +oid sha256:e2e18fc2414f134a6409a582a2bba428bdc6c3a3d07f209759a997aa4ca9342f +size 9020 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png index eb9103188..39bded808 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:734ded4b3f5b6a42f5a38ff65efee9d8466e5511f8b7c2492f36250a0d0f615c +oid sha256:2e2a99a9ba0f44742af59b7d55dcb1758541410fd0df83120dad553c0577eb73 size 11792 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png index f9714e303..d1417fb8f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:809d47db52fe7c6248704e6c4edf257e06365da15bde62140175a3fee534ccba -size 10040 +oid sha256:4ad8f0bba3425685db654e18781bfee25e587fb669b68d0b4e948c7000072ca2 +size 20130 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png index a09ecc748..e15e94223 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c1ab0671873d0ac224ef2303aacfbbec2acb2d914040ce2d5469e51fb5eea18 -size 18524 +oid sha256:a9ff15e1e9b776fe515cb7f7bbc4cb54a7556c5472788a860b99618d4d8a3578 +size 18516 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png index b8b94d90c..23ce361d7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:927f376922e21e380fbd943ddf9a13f14774d4d3b7110436b82364fa1889671a +oid sha256:b7d3de4ebb39ba8740a8329d7044056d5ff8ac924b6ee78032af36141ac6a4b3 size 1794 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png index 8ad0ef2cf..7aa5c5e0e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd3f480017119bc70192255dc3315f444747d1379bec915e7cc3dd771961cecf -size 2410 +oid sha256:4381267ab8e5c6d09487f79ee861ca4929eb91a7af5cfd3fd42a8664bac448ed +size 5349 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png index 689851571..34e6d4e26 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b23186fee34f325c965e4a7dc623caa6e71527b05aafa6089ed0e477993e4f6 -size 2656 +oid sha256:a263e68940d4a45c69b083461d92f71008cb9bd5d972f99a824ce1702ebb0676 +size 5780 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png index 48f1ff7af..b0bcecd96 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b970c12a4d204c04b54b9907d7206c5e3036a04e40cc6ef87e986509faa8fa4a -size 2376 +oid sha256:50ab386ba5cb492955b31ef1b60bfad24222e34cab065e9b5d8da7f9d18ef410 +size 5226 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png index c5042fd75..8bb66b90a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e32ebf0685fcc9d4e6b80e4d7ea474cc6d5a68138842fd35e6eddc7e0cdd0b8a -size 1579 +oid sha256:33161f003216fe713bf75717480719d1f1dafde24bfbd8fc31943c4861de3e0e +size 1658 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png index c5042fd75..8bb66b90a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e32ebf0685fcc9d4e6b80e4d7ea474cc6d5a68138842fd35e6eddc7e0cdd0b8a -size 1579 +oid sha256:33161f003216fe713bf75717480719d1f1dafde24bfbd8fc31943c4861de3e0e +size 1658 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png index 09f47d53b..20b2f2c5c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0623f52c89fe2d6b7ee46941999ee42f0c4c04b7a8d122874b1dfc4ca00f474f -size 1206 +oid sha256:824f947ccfadc702799556afe6b2c05ef5bddf453d0522b301fb837d255ff0ad +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png index 3a4538c89..eb08e65a4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cad09068867e5c56874dd5b44937fd22a386d27ff82e6c5d974444512f1950a -size 100398 +oid sha256:e3856dd885f97801efd250f858c6c287b4c0ab5b05dc9f6eca5defc2412b344b +size 100634 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png index f9a9c52a9..252662fab 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb62efe6efecb68e8897474cfc437b4f90dd254196128e91b6be2c11323200c8 -size 1425 +oid sha256:e826ac8ef1c72a6f2b050ce10a649f905269e47461a72137335fbbcdffd03ff8 +size 1972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png index 8fb3dcbc6..c10d3a435 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d26fc5cd84771e18b05aaa72c80ee39567a339a86385be7c0c498a550b0e6f0 -size 1452 +oid sha256:6fb2276b9f6c566156011a12a61d6517c7bc2069068a0eb1ea6e3f69a4a081a0 +size 1719 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png index b56336ea8..8bbe40ccd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b5ae76b167965938f543cfc6f80bd4b6b1c016616882a475e32b7c1319a81e1 -size 1468 +oid sha256:9e06f9d71559cccde2950eef23cfe7041e1e00f66d08cc3013b1489bb8bca161 +size 1719 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png index 95c378570..143ed026c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5c1044ad651b51aad6120355b235b3b04e984614fff53a498db000d92237f5c -size 1408 +oid sha256:0028dcad66a54d6bcad2b7f84e388b8bc71c65faf3e78afa74a00b348716bbb0 +size 1972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png index 12464c6fc..a85447471 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c13059790e8e8a5024b9362c57503f4888675ccc8b64f0eefb865ce2e7002906 -size 172 +oid sha256:8e6e3250beaa281fbbf35f2226cb850df1572aec477cdf438467a43332858ed8 +size 246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png index bb599f236..449b5dbed 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9963beae12b28e18942b4aad61c79494189116950432c53c6f99abe7485d2253 -size 1350 +oid sha256:15351ad42f0cd94694323203d78efdc0619777f8c20be2ca11ffb7ca1e4a7e04 +size 2135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png index 58e64a8ae..915421f84 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e3ab90923f0e249b6b2f542311ae57ef9921483f8f934e4d57437654abee240 -size 357 +oid sha256:a22b4c13787f4844a144a78a877f3bdf2909d14c978499239bd4b4bcd5ee7276 +size 359 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png index cf385db2f..99f68c1f0 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5511d05de6c5b7de1db013eb2dafb1b869ec353377829a89785c0ef3a4d5e41f -size 97 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png index cef683600..e7fe3a68c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15541d241c3e6d1d47388544731bd7c0da4d5533919dc791786193473ce788f1 -size 452 +oid sha256:d22933f51af79811941478c040765ad110220539e6b8a80f1482396728734c5e +size 459 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png index b9df1a69b..7b18e7521 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6aeb2dd8e99354cbd41360611642e9b286e646382dc224c5f9ce487aef5beba -size 483 +oid sha256:b96f610313379fdff50ad3b6a3f8f043cfdad79ea10b36d195ae43f984e5cda6 +size 492 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png index 8d8db722d..9eec6b510 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94d1f65c198c0e57405459392ffa2b6c36d64f7fe4053960216eb487bc4ed0fa -size 2607 +oid sha256:c48c78a126137422b0ecc63dabee18c728b8c64737eeae39127225c96fb497a6 +size 2603 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png index 317d43265..97cf16382 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39252d1bc31ac8ffca3e4975f87a8d15197a47e17506f5c4c857a6327db011ca -size 38416 +oid sha256:404640208415ee934115ba2d4682a27b3b8788c5ec5687cbe6b14bc952a60a29 +size 38463 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png index 0945fc432..996e13f38 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4721c27c827c3f716ad18ae1cafc32132ae47b6557a01d4c16acaa7b7d92400b -size 20601 +oid sha256:67182aea50200bf85bfcf29cc12fe96803293e88b92be1bd0c12db75095ed2af +size 20647 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png index 41994df52..0d399af32 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:662e4f78996d9c12d6698410543c6ef0aa474337b2d03084924b1c706a1e4916 -size 13462 +oid sha256:50554d4dedc99388aa93249ab0e29eb1c4cddd056096e9d52fa90ee31d6b34e7 +size 13418 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png index d24bd6b86..e3d781f7a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24f74782275e63f241431410af079089f2bd229ab06071a472de1a360ca934d7 -size 17093 +oid sha256:4fa77d6df6eb442520de655573e81f95db583a7f04603d6cb55357ef09d39a80 +size 17099 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png index ad5b15cb6..5aacd9ffe 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b08fd85ee90ab448f31ba0eb686142e93cd8d80e0cc6b8abb3a9f615bd5e5cb -size 1687 +oid sha256:920da31b03dedb1b761ace43694fcd919d4c2185d7f2656da054c34e67e0180b +size 1634 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png index 15431f30a..3bbe70b6a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02891cbdc2242395343290bc5403fd161fab49e470f93fd0c5639a93464f274b -size 1754 +oid sha256:171bd67abc692b7691b37ca1b377961cf0310aaf3353ff7bee09c5cf70de11b7 +size 1757 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png index 4e29cc25b..ea5ff7af2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f95be339c0fc7f9315968001722777d1eebddbf6eea41c8d9d524b8775842763 -size 2024 +oid sha256:12cf2ba0107edcc9e54718ba760bfe5c64c6e34279fe7c906a4cbfb044a89775 +size 2012 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png index 3fe215ed7..c8d180a07 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86280439d207ed0a74595757af265a313d75f7ff8b7502eb17d2d7184855eb12 -size 2499 +oid sha256:e0dd9c3584229f6e9b488e4e63474a059c082b89c26609cca00769a53056a598 +size 2518 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png index 8ad422f6d..8bc1b3d99 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d762246aeec860558a8ee8e5318126937be11a9561a4bd1ff18f90900858bc2b -size 2852 +oid sha256:45d30617520b3153f72457ca3cc9ddc8f930427b702105f2ad3d2c05c7579203 +size 2848 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png index c7cb00188..b2cd63278 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20457c79f2f5a782088bc4f9a333d0092ba9c5f5307835cdfc3ca7bf527420e4 -size 3247 +oid sha256:889ffce3febe8de6ceca2ac67bbc41a2011d2e2104ab0f7e0a9e7401bd96dc5a +size 3258 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png index 12ecc129c..6938b2f2c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b81ef6b2a5b4f848d740f1ff9de08c90c0dc1e7ef3d11b43c2b704b29546c26d -size 2806 +oid sha256:96e62fdc1c5505eff8e68b691e2d9514049dcb4fd1b971577d929e210876afa8 +size 2451 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png index d6c24cbf4..7effefd17 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:145da3c382448615cf0b90f4718b8a79a8b2d7f3dd33042c755e15d5b127d33b -size 2788 +oid sha256:8ac308624980962df3442cf2542ed90962083afc05eeef02e528b5499e308a26 +size 2521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png index 7c67a4ece..8e2493ed5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:779a0d18611eb44bead0f9dbd704c3c9e10fbf92e906fdd4281075f3d5c946f9 -size 2906 +oid sha256:d95a4146084f030ed33fe46a564010d699a9d412a62bfab8ec7866c722d780fa +size 2529 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png index 5be0df234..394b09302 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac469abbc75f28cfb40ff8dc84879c6a5a92b0259ae87cd475e3c2df48b2cbbd -size 5421 +oid sha256:cbe37bed60d319bb0f9f9b953d8872635b30074f384ae2ea8c33159d7ee7e931 +size 5418 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png index dde4f7412..add0c5018 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22a71b4f18cae498e33ba68c137f8329d903f59976296040890fb4395f8b56f5 -size 2854 +oid sha256:a2dfcfe9f8e155a6144b67bd4815c3f20ad34003c1db1437027da4f9d30e59d0 +size 2878 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png index 283497551..d7732392d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d1456250f5dd7a33d8719363f718ebc414bcce7459cb9bf17780f79f0a1e313 -size 2940 +oid sha256:51f32a7e3fea8df51451d5ebcca9322624cf78a9d858a602443a92ab9daf9bf8 +size 2985 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png index 37f181075..76fa75439 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:309caf3e21c0b0cc0748d270cc6f40e2507a4b297996155adba75981c39feda7 -size 1558 +oid sha256:c9c4fd1beb18f332de4e5c7f2f2acaa66896ab26ff0f987c5f568702daad9dc7 +size 1586 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png index a6628377d..00879ae12 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:963e72b72ee771576aa3af1c2da943efb9b63e45f370f990717a8b472eac73ff -size 2855 +oid sha256:9b1df5961ad58ee84780740fafd495c60dccec0f3662c4af90fd019251cf444e +size 2868 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png new file mode 100644 index 000000000..918d647a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2daedacd9a16eb276da4fabf41447e568d133a239d69cf7bfa0d1a7132d41e90 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png new file mode 100644 index 000000000..345a61049 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474135f94f16c3a874b0b8a69b1f244224fa1b3bcbb5fad084eb1dc6eb3bb064 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png new file mode 100644 index 000000000..ee5805b46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5940c044f15bb6ab4ee9b60e7e11d8d587bba9b510958ac8d0bd1e924c7a21 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png new file mode 100644 index 000000000..1a354b3ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83195d69738bf815d169ac86a0dda8fc3996ed78d46b27c22e572efa64d3a65e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png new file mode 100644 index 000000000..3d04f2ab3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:683565c4a7dd7a20d60e11cf22ba7c178bec285025868d87a9cab7cddbd667d4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png new file mode 100644 index 000000000..6f2a37c7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609f9c6c4fafc8babda5eb4ae5155d1b83e1527ec2af495d82e3ce721c2c9e41 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png new file mode 100644 index 000000000..f1ead9c08 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2e607ec3da2e0c7f3ad1085f7394730246381a2b707c006662dc334871c03d +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png new file mode 100644 index 000000000..d4f6ee246 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4780e3b175e966618f9d0ba070a6bcc8e8feebfb2be4c572870ad31b61e7c1 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png new file mode 100644 index 000000000..918d647a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2daedacd9a16eb276da4fabf41447e568d133a239d69cf7bfa0d1a7132d41e90 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png new file mode 100644 index 000000000..345a61049 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474135f94f16c3a874b0b8a69b1f244224fa1b3bcbb5fad084eb1dc6eb3bb064 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png new file mode 100644 index 000000000..ee5805b46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5940c044f15bb6ab4ee9b60e7e11d8d587bba9b510958ac8d0bd1e924c7a21 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png new file mode 100644 index 000000000..1a354b3ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83195d69738bf815d169ac86a0dda8fc3996ed78d46b27c22e572efa64d3a65e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png new file mode 100644 index 000000000..3d04f2ab3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:683565c4a7dd7a20d60e11cf22ba7c178bec285025868d87a9cab7cddbd667d4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png new file mode 100644 index 000000000..6f2a37c7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609f9c6c4fafc8babda5eb4ae5155d1b83e1527ec2af495d82e3ce721c2c9e41 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png new file mode 100644 index 000000000..f1ead9c08 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2e607ec3da2e0c7f3ad1085f7394730246381a2b707c006662dc334871c03d +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png new file mode 100644 index 000000000..d4f6ee246 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4780e3b175e966618f9d0ba070a6bcc8e8feebfb2be4c572870ad31b61e7c1 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png new file mode 100644 index 000000000..113c9e069 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577cff471034b801e84c2df271946e59e441d0890910b949dcd7b81b25f38d58 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png new file mode 100644 index 000000000..d406a3275 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8df185b0b10595bba92b871646a6b349b308221e63c2ead096e718676716bddd +size 72 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png new file mode 100644 index 000000000..4c6092dfc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e7483e76c3b65b94b68499f93f07b2c73435353adf46638c2d1fe16d62f6a0 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png new file mode 100644 index 000000000..af764cb13 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2eeed8c081a355f23062a56ea80f1891745c63f8468d65771e49418b508cd6 +size 119 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png new file mode 100644 index 000000000..0a126de11 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c24b47a3afd5d7185d9722cd8f0bd4274489bedd4e09c8d23a66e49af405dc2 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png new file mode 100644 index 000000000..8570310b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c70c09a82dfbb4db1955e417c1f24ea90178f5234bba420e71f74a094218c7c +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png new file mode 100644 index 000000000..3730d9c35 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfc380297d61413eec1b9a3f634ae0489c22fb310b004e1ecf6ea26ecc28b5f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index ea923d341..73217aad8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:391cb9c926dd579f1aa0ed327e4d3a8509139cdacae4f3b0edc97106f79dd9b2 +oid sha256:9d15ab9435f0c2167443434422c420a6c26e7749f88c12751dd6f74cc4f013f7 size 17378 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 2de4910f5..32d0548e6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:691dba96a0bae7fdbd18ec903d842342e3bb76e2ce921bf615f705a3b0d309f7 +oid sha256:2fbe28e31e46b156fe7017628b4cd530f8211bf16b4cb15c335e51e190ed7f77 size 778 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 28988a333..08dc9ee18 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2183370685492ce65749a6c320ffc821131adb291c12163ebe185a2e2f707965 +oid sha256:229b4930840c10211e554f63ebb8c5701830ea27eb298f65cd6ddcb99ee1c3fb size 16823 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 1c981f991..673944323 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e44002ae9eb867c91406a82dada3b768cc9aa300f931b511ac602a18c21178f -size 15102 +oid sha256:8ec38824fbaf150d9924f9d53b4fea2a5efef7cb782c81591079de4010a226fc +size 15109 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index c7be2c3f7..4a3df2f4d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c64c88b72b5a1018e46b8234025c4706fc362b746737e6c04dc01e36f15189b0 -size 725 +oid sha256:9637e8c6e2fb90bb4182c55daf13e604a03cf68ab36c79c2c97f016768df8df6 +size 726 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 85315b4a4..ac17b1fd5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bcbf518dc950feeef34927feb3c89068ceb5412ba0bdb0a106e69b723dc8910 -size 15498 +oid sha256:71be9cac7c1015c4b9139433d7585b7073c6242bd7e1d7bb1c1194d6facdc8f8 +size 15495 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png index 9780a7767..8a9192058 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2911ef69f673be85d75ca8b70f4039823290fdc3096caa0ef792d541bd531b9f -size 115331 +oid sha256:b9d8d6a0327d1d248476e52e6d5926757989471a9bd300a4b84af92efe58d1af +size 232019 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png index cfd648192..9bfbfd1e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae6ee08cb58592e49582e3543f99beb955e0e343a874af053098949cef1e25d8 -size 11040 +oid sha256:00b742a18ee40eaa558de7649ee939935459e7a47beb54f80a7c735f584a7afc +size 21475 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 3e68f9b77..ab6e2d683 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f05a32ebdbdca54454ea2624d085cfd4965cf676bbad36f9be9ad199a3b7faa8 -size 604 +oid sha256:e347dfb9591f3db40f5692ee7ade9b410784eb06723c92558f5b275382c826b6 +size 605 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png index e243c035f..130361f99 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:803037eed7e876797e3920b3b8b1c7874a90affed7360c7911be63405ab37a08 -size 10630 +oid sha256:f788f1e86060b61c7d52b3c0efdd654e66a2fdc5fc90e68a76f6018ef9cd9f8a +size 10604 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png index cd00ebe1a..1cf570754 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03f8b2b0340e28882217f09502961d26422905144bd55681627a82b02fcc3f42 -size 15823 +oid sha256:0b62d4300ed2d7cdc7215c554200f6cd3b96daaa00b00de7a7b2228babe90e69 +size 15834 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png index ca42e83e7..629502a16 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fd14861fa01d9dc06a0bd2872ff24547cb366784c2d6af35b687e21783ca5f0 -size 1083 +oid sha256:be46a8d83553144fac730baecd57326e47af2ff18b80cdcfcc8029ae9132115d +size 1082 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png index 46ca78bc8..bbf462234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ae72474cd3fa4ca95f93a82fd7b7f544c06f7307faf293151e1d2ce0433fbc1 -size 5234 +oid sha256:43826aabef30a85430692d0d8f2d391f7b7c51c2f748531089cdd19c0347aef3 +size 5233 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png index 6d0f59b16..dd73076e9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd361bad89a3ad48ca0e54b7493f51cfde973f19c44ff3e8af3179bdfb30a9c2 -size 14692 +oid sha256:81de087ca73048b8a8063887fafa049b151d5eb819962447ae7e93a10276053f +size 14684 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png index f6c0883a1..eba451cd6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a3ed8e0c4188a81e77da1d5d769865d8de26076f6a60a358bea96299c00718a +oid sha256:4923300182d4227bf8f285c47a09e7544d39acdd629076d6de9830a47d3f3b8b size 1000 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png index 59db80f48..1aa15bfc1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6223e0db0e24f739b50afb77cf3cb18c6043fe95cc643a201fd70df1c5ef2da4 +oid sha256:a40e171a28a3620bb582d30be4281ddabd94f11302f65aa63ab7d4ab4e7f7ca5 size 5021 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png index 44f4e51a9..44e996e9b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab5e2ebd26e2c828c0ecde3e6a711adc1bf595ed08581ba223db6a36433b8dee -size 295 +oid sha256:2048b8b8ff6435fbd32f4966cb2a15edd9f32d59a3faccef05333da08f9040a3 +size 293 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png index c5a94188e..62a8f02ea 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536d6c42b490c383833605478e4a9875b41e01c877136b696b4b5ea204f269ff -size 80945 +oid sha256:eab1ca9869fb7930ee5bee6ecfbfe4692b7eeb009f2d0323132896356ec79fe4 +size 80947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png index a2e70e54b..40993557f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ab6952916f050467e1caf24f2d8106db7e7778d83ffd13117b5dafd5b2a9106 -size 407761 +oid sha256:7f1418f50b3831f6aa838b70bdaf7765a5ddddb0a693c4e8c3537e06a0a70e98 +size 407764 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png index 0ee6e7102..6fbc1665c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:617e6041a8f312f0890d0b287f1a7191407a83b7aa3f47bdf214ae702ef32ee1 +oid sha256:0703eb4a553d757637c38858c3115dbe5462c391288ef271cdb53fef211a096d size 36248 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png index 3c27e680d..8ee9484f2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e792c1b683634907b942c45e4693121a77d5f0184e59124f78ed936f131de63 +oid sha256:a7b52e2255f8f1a7b72bb6a6937580d87cddd3b339a7894f5ec3f052b8485c64 size 407 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png index a06918917..5e223bab7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1d8c462a23afc5b2558e3d044c71a21af222ef79101993c0c302dc508d6eb26 -size 486 +oid sha256:d4b9e8c04f5984bf697f0c45bd98c471e86d020bcd9068a0ba9b6a394dc62543 +size 451 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png index 6d70f32c2..0b2bf7e41 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f839ffbac1b001539912b2759206d2b3de2235f059e487505e5fb6226396c531 -size 184457 +oid sha256:4c6bf01e2e38c30fc1a12d208d6264d43e6359fc202de37ce137479738720084 +size 184460 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png new file mode 100644 index 000000000..362818a52 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5dfd12a315d54dad1931e83738fe84f9d681a90aeb6cd4df0886668ae78f686 +size 1752 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png new file mode 100644 index 000000000..6ffabd4f5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:305b538606ca3005522be3b51e76128503427dd21a212ef59797e6d26553ec31 +size 36499 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png new file mode 100644 index 000000000..8c5045037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e50f8be7a061a595eda81eda41dadc368f9fb1ed7044d03599147b612a7f302 +size 21154 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png index da297a340..239a2e5ad 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fb52782b011b71d76299c1befc26eef0a0736606dd302dbeb787638d070dd76 -size 2288 +oid sha256:5771531a0e424de19edb9b59c759df3a3ac5d769d4e5495a67179590f3fe8bd9 +size 2038 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png index 58ed7c23a..05cecb028 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800ce6ca4f9e7be5417df968ae74e094e7bb2a0823d033d6cfc0e733b8edb848 -size 2288 +oid sha256:b4267e18ce07a5b0dccef1cfb6e96968e4cd66939d3f411a2fa476035165794c +size 6517 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png index 58ed7c23a..05cecb028 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800ce6ca4f9e7be5417df968ae74e094e7bb2a0823d033d6cfc0e733b8edb848 -size 2288 +oid sha256:b4267e18ce07a5b0dccef1cfb6e96968e4cd66939d3f411a2fa476035165794c +size 6517 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png index a2a98cf0b..26c5cd268 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dacc58e79528708bb121de4e70a6fcfcd16749903120419461c292b8abd0569 -size 4694 +oid sha256:f28dcd171fbd0d90f3f36d155f086d7d054d21a86cc637353ede310920238baa +size 4678 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png index 8f178fd54..0fe683c1d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4777dbbd1b362e681a77bad5463048d3891d1dafe2f7c0849772f45653c3a1c -size 10941 +oid sha256:e06b27fd2bb367ac3ed7c57777a5073ccacc99ba99bbbdfef238f905a7c58cee +size 4693 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png index 8f178fd54..0fe683c1d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4777dbbd1b362e681a77bad5463048d3891d1dafe2f7c0849772f45653c3a1c -size 10941 +oid sha256:e06b27fd2bb367ac3ed7c57777a5073ccacc99ba99bbbdfef238f905a7c58cee +size 4693 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png index dd5a2ace1..162e2d0a6 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f94cb03a84826ff994379c9203937a92dfbe83af80986ded3c567645713c6f6 -size 4890 +oid sha256:ca43950637e94a3cc27ece19a223325ae3d4b30e25e79ec3ebcf00cc91c8cc9a +size 4883 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png index 7024ebfef..e4dcbc49c 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 -size 4885 +oid sha256:0553e98786c9f025abe2acc8aa86fa6fed02909d6524b5ca847600a79709adb7 +size 6513 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png index 7024ebfef..e4dcbc49c 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 -size 4885 +oid sha256:0553e98786c9f025abe2acc8aa86fa6fed02909d6524b5ca847600a79709adb7 +size 6513 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png index 39f290334..6ffabd4f5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81717e72015b32fcffa1a59d931a843b5b1673dc8ffbff638f2490fd009ad180 -size 36496 +oid sha256:305b538606ca3005522be3b51e76128503427dd21a212ef59797e6d26553ec31 +size 36499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png index 1af25e3e1..3b25b6ccb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f41a149e527d72a3b2b94948b625a15f9b73f5794a636225d4b5354f4b305bb4 -size 6938 +oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d +size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png index 1af25e3e1..3b25b6ccb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f41a149e527d72a3b2b94948b625a15f9b73f5794a636225d4b5354f4b305bb4 -size 6938 +oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d +size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png index 1b1ed3e3b..64b7db546 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c -size 826 +oid sha256:98b346983c2204e1df96b17a68817c39d97f754d2250a5d293ae3715bc5c6893 +size 4153 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png index 1b1ed3e3b..64b7db546 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c -size 826 +oid sha256:98b346983c2204e1df96b17a68817c39d97f754d2250a5d293ae3715bc5c6893 +size 4153 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png index 0d51f838c..9d2de9c91 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd -size 804 +oid sha256:3ede5d07fadc0fd9584fb0816d30d7d44482e0df58dc357f8657b1e175cc6e2f +size 3779 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png index 0d51f838c..9d2de9c91 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd -size 804 +oid sha256:3ede5d07fadc0fd9584fb0816d30d7d44482e0df58dc357f8657b1e175cc6e2f +size 3779 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png index 00a793ec2..f2e9762e2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 -size 798 +oid sha256:fb8b2d1ca50be5e745fa5e8a2a303fae80440b4b953da8b4cdb32d126527482a +size 3335 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png index 00a793ec2..f2e9762e2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 -size 798 +oid sha256:fb8b2d1ca50be5e745fa5e8a2a303fae80440b4b953da8b4cdb32d126527482a +size 3335 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png index 443c5e78e..4837b94a1 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de -size 781 +oid sha256:b91be13b16fc40fa5253d3c0839cbe0a5532dc72e42bb85e2f3e8224d3201ef5 +size 4186 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png index 443c5e78e..4837b94a1 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de -size 781 +oid sha256:b91be13b16fc40fa5253d3c0839cbe0a5532dc72e42bb85e2f3e8224d3201ef5 +size 4186 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png index c561128ef..1e1ded962 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 -size 471 +oid sha256:dca773106b7ba2857ed764535e536dcb08b2be55deff78c5494be8bad4bb8f9a +size 2947 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png index c561128ef..1e1ded962 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 -size 471 +oid sha256:dca773106b7ba2857ed764535e536dcb08b2be55deff78c5494be8bad4bb8f9a +size 2947 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png index 1ad01578b..3aed91e90 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 -size 471 +oid sha256:a0021289109332c93e7cb92733d57466c6f9aa3f9cdf24302876078a208476cc +size 3010 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png index 1ad01578b..3aed91e90 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 -size 471 +oid sha256:a0021289109332c93e7cb92733d57466c6f9aa3f9cdf24302876078a208476cc +size 3010 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png index 55a946401..2045f5391 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 -size 491 +oid sha256:74c5cf879133de1afbd22980222469fe169899108d8a823e63174120fc681dec +size 3295 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png index 55a946401..2045f5391 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 -size 491 +oid sha256:74c5cf879133de1afbd22980222469fe169899108d8a823e63174120fc681dec +size 3295 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png index 5fc00164e..883df5636 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c5631a6d7a0f4278500f78a5e9e8ce25992b35b0f2fae5d00eab8a21ed6f95f -size 3682 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png index 5fc00164e..883df5636 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c5631a6d7a0f4278500f78a5e9e8ce25992b35b0f2fae5d00eab8a21ed6f95f -size 3682 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png index 096f34c82..36b58fc84 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e -size 12939 +oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 +size 12961 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index cf5b2640e..88aa5c7f2 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cc025e5fffdbcc7c3b97755e87bb02ceeb837dbc7ca810ab14434b116ac554d -size 106 +oid sha256:cc4d11f391889b6491624ec21edcf0928589fdc19bb81789ebb2e8ef1de62f73 +size 190 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index d93b91a30..88aa5c7f2 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84eff105c799ed23497870d1a13d2e69986cf7240da2d508794b2974bee1c5b6 -size 254 +oid sha256:cc4d11f391889b6491624ec21edcf0928589fdc19bb81789ebb2e8ef1de62f73 +size 190 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index f0cad6422..99d393223 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3581673cd0c053326d7c6947d232f62a7c0c61f3b86aa881be40ae609278100c -size 1188 +oid sha256:33b606c57a64b6fb4efedf17ad69d21b877c6ce67d96442cfa89a5807a93e52f +size 1097 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index 3e0bec9ce..e00478bdf 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ccc8f2a14e5a3c8f7aca9c9411dbff01d8e1a3c7dbd66d515881bd753ebf922 -size 1284 +oid sha256:a6b5857f7baa68ded78bd2101bfa1f21efb95109589e71dc692850bb72931b6e +size 1152 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index 78e2037c1..2fb3f90f0 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5befd1a942a8b50b40b1f4e2938699b84239d028bd83e31445271ec5f2043c64 -size 1238 +oid sha256:a68b8a9d9f7cecf38b204403607ad1b5b98cfe82efa86addbcd9da3be3e189d2 +size 1130 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index a4f0a8bfa..c1ee69bf7 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e491edd79708d65a78d7eec5e32f4ec108d2b73fdc19ea614f09ea02f8e4183 -size 1373 +oid sha256:7e886fe91f9b2afda61c131cbb7ef9fe00a0cc5cbe039e7fab0565259cd595ea +size 1310 diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 956f6473a..7cca80360 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a80eed08bfbf24ab5b9a7503c8751cb8ad476e2e3f5569d405d3e4a8e88bf5b9 -size 116734 +oid sha256:d060360cba0713727e6dbab45f30fce0890987fec87933c860b3d459d26f21f3 +size 116720 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png index ce83d58cf..fccfaa9f0 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f8f2d2f9f855e726e8075c400aab4edc2ebd128b539d35f1dff37d4f02669d1 -size 31939 +oid sha256:1b359e29a789870afec6bb043472faae4d9ff9f5f4c994fc55436ffb7d80c1e9 +size 31935 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index 9433427aa..f174b8178 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e075f71a20f3fb8957b2412820eb533715ee3968d46a6454c9713b3f0d4641f +oid sha256:0981d21ed8f75ee1fffefdae75505365bea3715bfbf8bda56d278792766f9b09 size 10939 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png index c23244fc0..91c33671a 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:058edc03c921cbd6fe1da9df82000a453c4d88dcfbd1a5fc756be854c2053f45 -size 31954 +oid sha256:ccfe86f8c60ff7fc1839959712b039eb9c32cc4cd1c3c182fb55ae83a2f3f1f3 +size 31952 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index 5b46fff4a..681c0543a 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:084c39dc74b3cc84d16b057e785fa6576a09bac3aee87437b3de10c7b4f99fd4 -size 10939 +oid sha256:53206cc3329cb5a49afd48cb17a98a4ced8b38bc6f0b90b5f02e647b0f23e8ee +size 10940 From 67d80786fffabe0ced7b398c66b770e2f14a99b9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 23:06:29 +1000 Subject: [PATCH 096/136] Update Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png --- .../Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png index 0428bd7d6..27c560fea 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb3a3b312deb41f7faf1b6ce06538496903c1be02672400ba760e8a0eca7171f -size 3653 +oid sha256:23f3f42a3dbe57707ce5f488b1495a58b213577a9a06a45babb28b65abf1606b +size 7127 From c7ae44631ecc1d04e7cf445f14a0d34bc92aaa76 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 23:15:45 +1000 Subject: [PATCH 097/136] Bump tolerance --- .../Processing/DrawingCanvasTests.StrokeOptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs index 20072c184..142c4ea1a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -49,7 +50,7 @@ public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider canvas.Flush(); target.DebugSave(provider, appendSourceFileOrDescription: false); - target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0001F), provider, appendSourceFileOrDescription: false); } private static IPath CreateBowTiePath(RectangleF bounds) From b5c1f8a7a7452b16b2f09cbfb44ec230d0b1e0b0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 00:49:55 +1000 Subject: [PATCH 098/136] Fix #344 --- src/ImageSharp.Drawing/InternalPath.cs | 57 +++++++++++++++++-- src/ImageSharp.Drawing/PathBuilder.cs | 9 +++ .../Issues/Issue_344.cs | 37 ++++++++++++ ...rocessWithDrawingCanvasTests.Primitives.cs | 1 - ...uilder_Rgba32_Solid100x100_(0,0,0,255).png | 3 + ...verlap_Rgba32_Solid100x100_(0,0,0,255).png | 3 + 6 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs create mode 100644 tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png create mode 100644 tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs index 1af0919fd..58fc69610 100644 --- a/src/ImageSharp.Drawing/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -225,16 +225,47 @@ private static PointData[] Simplify(IReadOnlyList segments, bool i { List simplified = new(segments.Count); + // Track indices where collinear direction reversals represent user-intended + // geometry: interior points of multi-point linear segments, and junction + // points between two linear segments (e.g. PathBuilder LineTo → LineTo). + // Reversals at all other indices (flattened curves, curve junctions) are + // artifacts and should be removed normally. + HashSet? linearReversalIndices = null; + ILineSegment? prevSeg = null; + foreach (ILineSegment seg in segments) { + int start = simplified.Count; ReadOnlyMemory points = seg.Flatten(); simplified.AddRange(points.Span); + + if (seg is LinearLineSegment) + { + // Interior points of a multi-point linear segment (e.g. DrawLine with 3+ points). + if (points.Length > 2) + { + linearReversalIndices ??= []; + for (int i = start + 1; i < start + points.Length - 1; i++) + { + _ = linearReversalIndices.Add(i); + } + } + + // Junction between two linear segments (e.g. PathBuilder LineTo → LineTo). + if (prevSeg is LinearLineSegment && start > 0) + { + linearReversalIndices ??= []; + _ = linearReversalIndices.Add(start); + } + } + + prevSeg = seg; } - return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear); + return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices); } - private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear) + private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear, HashSet? linearReversalIndices = null) { int polyCorners = points.Length; if (polyCorners == 0) @@ -294,9 +325,27 @@ private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, { int next = WrapArrayIndex(i + 1, polyCorners); PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]); - if (or == PointOrientation.Collinear && next != 0) + if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0) { - continue; + // Preserve collinear points that represent a direction reversal (U-turn) + // within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point + // is collinear but the stroker needs to see the reversal. + // Don't preserve reversals at segment boundaries — these arise from joining + // different path segments (e.g. arc-to-arc) and are not user-intended. + bool preserve = false; + if (linearReversalIndices == null || linearReversalIndices.Contains(i)) + { + Vector2 incoming = (Vector2)points[i] - lastPoint; + Vector2 outgoing = (Vector2)points[next] - (Vector2)points[i]; + float inLen = incoming.Length(); + float outLen = outgoing.Length(); + preserve = inLen > Epsilon && outLen > Epsilon && Vector2.Dot(incoming, outgoing) < 0; + } + + if (!preserve) + { + continue; + } } results.Add( diff --git a/src/ImageSharp.Drawing/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs index c29510e4f..adfb3c38d 100644 --- a/src/ImageSharp.Drawing/PathBuilder.cs +++ b/src/ImageSharp.Drawing/PathBuilder.cs @@ -110,6 +110,15 @@ public PathBuilder MoveTo(PointF point) return this; } + /// + /// Moves to current point to the supplied vector. + /// + /// The x-coordinate. + /// The y-coordinate. + /// The + public PathBuilder MoveTo(float x, float y) + => this.MoveTo(new PointF(x, y)); + /// /// Draws the line connecting the current the current point to the new point. /// diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs new file mode 100644 index 000000000..697cf3db0 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_344 +{ + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void CanDrawWhereSegmentsOverlap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1); + canvas.DrawLine(pen, new PointF(10, 10), new PointF(90, 10), new PointF(20, 10)); + })); + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void CanDrawWhereSegmentsOverlap_PathBuilder(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + PathBuilder pathBuilder = new(); + pathBuilder.MoveTo(10, 10); + pathBuilder.LineTo(90, 10); + pathBuilder.LineTo(20, 10); + + Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1); + canvas.Draw(pen, pathBuilder); + })); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 90b65c125..83ecd7485 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -3,7 +3,6 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png new file mode 100644 index 000000000..015bd5aa2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 +size 111 diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png new file mode 100644 index 000000000..015bd5aa2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 +size 111 From da774603dac9b9ba1acb8f01c6712046d2d5785b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 01:04:17 +1000 Subject: [PATCH 099/136] Fix #367 --- .../Issues/Issue_367.cs | 36 +++++++++++++++++++ .../Issue_367/BrushAndTextAlign_Rgba32.png | 3 ++ 2 files changed, 39 insertions(+) create mode 100644 tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs create mode 100644 tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs new file mode 100644 index 000000000..c32ac5ffd --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_367 +{ + [Theory] + [WithSolidFilledImages(512, 72, nameof(Color.White), PixelTypes.Rgba32)] + public void BrushAndTextAlign(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.IsWindows) + { + return; + } + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.Green, 1); + Brush brush = Brushes.Solid(Color.Red); + + Font font = SystemFonts.Get("Arial").CreateFont(64); + RichTextOptions options = new(font); + + canvas.DrawText(options, "Hello, world!", brush, pen); + }), + appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png new file mode 100644 index 000000000..aac6fb6ed --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad7ff052941d7371a8403ebf44c916e479b9bcd7e41ca08475b8bde5817b661d +size 8867 From a95d9108713d0885def9545915b6dd2627bd977b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 01:10:22 +1000 Subject: [PATCH 100/136] Fix #244 --- .../Issues/Issue_244.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs new file mode 100644 index 000000000..280173539 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_244 +{ + [Fact] + public void DoesNotHang() + { + PathBuilder pathBuilder = new(); + Matrix3x2 transform = Matrix3x2.CreateRotation(-0.04433158f, new Vector2(948, 640)); + pathBuilder.SetTransform(transform); + pathBuilder.AddQuadraticBezier(new PointF(-2147483648, 677), new PointF(-2147483648, 675), new PointF(-2147483648, 675)); + IPath path = pathBuilder.Build(); + + IPath outline = path.GenerateOutline(2); + + Assert.NotEqual(Rectangle.Empty, outline.Bounds); + } +} From cde991c0daa49abfbf89534e654909881ffb7a01 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 13:37:22 +1000 Subject: [PATCH 101/136] Add AntialiasThreshold and aliased mode --- .../Shaders/CompositeComputeShader.cs | 14 +- .../WEBGPU_BACKEND_PROCESS.md | 4 +- .../WebGPUDrawingBackend.cs | 30 ++-- .../ImageSharp.Drawing.csproj | 6 +- src/ImageSharp.Drawing/PathBuilder.cs | 4 +- .../Backends/DefaultDrawingBackend.cs | 5 - .../Processing/Backends/DefaultRasterizer.cs | 36 +++-- .../Processing/Backends/IDrawingBackend.cs | 11 -- .../Processing/Backends/PolygonScanning.MD | 21 ++- .../Processing/Backends/RasterizerOptions.cs | 15 +- .../Processing/DrawingCanvas{TPixel}.cs | 6 +- .../Processing/DrawingOptions.cs | 12 +- .../Processing/ShapeOptions.cs | 11 +- .../GraphicsOptionsTests.cs | 10 ++ .../Issues/Issue_134.cs | 51 +++++++ .../Issues/Issue_367.cs | 1 - .../Backends/SkiaCoverageDrawingBackend.cs | 4 - .../Backends/WebGPUDrawingBackendTests.cs | 40 ++++++ .../Processing/DrawingCanvasBatcherTests.cs | 35 +---- .../Processing/DrawingCanvasTests.Process.cs | 4 - ...thDrawingCanvasTests.AntialiasThreshold.cs | 133 ++++++++++++++++++ .../RasterizerDefaultsExtensionsTests.cs | 4 - .../DefaultRasterizerRegressionTests.cs | 8 +- .../Rasterization/DefaultRasterizerTests.cs | 6 +- .../TestUtilities/GraphicsOptionsComparer.cs | 1 + .../LowFontSizeRenderOK_Rgba32_False.png | 3 + .../LowFontSizeRenderOK_Rgba32_True.png | 3 + 27 files changed, 368 insertions(+), 110 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs create mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png create mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 4bf672c35..8a5b57c5e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -51,6 +51,8 @@ struct Params { solid_g: u32, solid_b: u32, solid_a: u32, + rasterization_mode: u32, + antialias_threshold: u32, }; struct DispatchConfig { @@ -766,7 +768,7 @@ fn rasterize_edge(edge: Edge, band_top: i32, band_left_fixed: i32, clip_top_fixe rasterize_line(clipped.x0, clipped.y0 - band_top_fixed, clipped.x1, clipped.y1 - band_top_fixed); } - fn area_to_coverage(area_val: i32, fill_rule: u32) -> f32 { + fn area_to_coverage(area_val: i32, fill_rule: u32, rasterization_mode: u32, antialias_threshold: f32) -> f32 { let signed_area = area_val >> AREA_SHIFT; var abs_area: i32; if signed_area < 0 { @@ -794,6 +796,14 @@ fn area_to_coverage(area_val: i32, fill_rule: u32) -> f32 { coverage = f32(wrapped) * COV_SCALE; } } + // Aliased mode: snap to binary coverage using threshold + if rasterization_mode == 1u { + if coverage >= antialias_threshold { + coverage = 1.0; + } else { + coverage = 0.0; + } + } return coverage; } @@ -923,7 +933,7 @@ fn cs_main( cover += atomicLoad(&tile_cover[py * 16u + col]); } let area_val = atomicLoad(&tile_area[py * 16u + px]) + (cover << AREA_SHIFT); - let coverage_value = area_to_coverage(area_val, command.fill_rule_value); + let coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold)); if coverage_value > 0.0 { let blend_percentage = u32_to_f32(command.blend_percentage); diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index a6a70b4a0..a56f8d057 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -23,6 +23,7 @@ DrawingCanvasBatcher.Flush() -> use target texture view directly as backdrop source (no copy) -> allocate transient output texture for composition bounds -> deduplicate coverage definitions across batches via CoverageDefinitionIdentity + (keyed by path, interest, intersection rule, rasterization mode, sampling origin, antialias threshold) -> TryCreateEdgeBuffer (CPU-side edge preparation) -> for each unique coverage definition: -> path.Flatten() to iterate flattened vertices @@ -52,6 +53,7 @@ DrawingCanvasBatcher.Flush() -> X-range spatial filter: edges left of tile only update start_cover -> barrier, then each thread accumulates its coverage from shared memory -> applies fill rule (non-zero or even-odd) + -> if aliased mode: snaps coverage to binary using antialias threshold -> samples brush (solid color or image texture) -> composes pixel using Porter-Duff alpha composition + color blend mode -> writes final pixel to output texture @@ -87,7 +89,7 @@ Each edge is a 32-byte `GpuEdge` struct (sequential layout): ### Command Parameters -Each `PreparedCompositeParameters` struct contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, and blend percentage. +Each `PreparedCompositeParameters` struct (26 × u32 = 104 bytes) contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, blend percentage, rasterization mode (0 = antialiased, 1 = aliased), and antialias threshold (float as u32 bitcast). ### Dispatch Config diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 86b083752..6601c41bd 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -162,14 +162,6 @@ public void FillPath( target.Bounds.Location)); } - /// - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel - { - this.ThrowIfDisposed(); - return IsSupportedCompositionBrush(brush); - } - /// public void FlushCompositions( Configuration configuration, @@ -733,7 +725,9 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out (uint)command.GraphicsOptions.ColorBlendingMode, (uint)command.GraphicsOptions.AlphaCompositionMode, command.GraphicsOptions.BlendPercentage, - solidColor); + solidColor, + command.GraphicsOptions.Antialias ? 0u : 1u, + command.GraphicsOptions.AntialiasThreshold); parameters[commandIndex] = commandParameters; commandIndex++; @@ -1623,6 +1617,7 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv private readonly IntersectionRule intersectionRule; private readonly RasterizationMode rasterizationMode; private readonly RasterizerSamplingOrigin samplingOrigin; + private readonly float antialiasThreshold; public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition) { @@ -1632,6 +1627,7 @@ public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition) this.intersectionRule = definition.RasterizerOptions.IntersectionRule; this.rasterizationMode = definition.RasterizerOptions.RasterizationMode; this.samplingOrigin = definition.RasterizerOptions.SamplingOrigin; + this.antialiasThreshold = definition.RasterizerOptions.AntialiasThreshold; } /// @@ -1645,7 +1641,8 @@ public bool Equals(CoverageDefinitionIdentity other) this.interest.Equals(other.interest) && this.intersectionRule == other.intersectionRule && this.rasterizationMode == other.rasterizationMode && - this.samplingOrigin == other.samplingOrigin; + this.samplingOrigin == other.samplingOrigin && + this.antialiasThreshold == other.antialiasThreshold; /// public override bool Equals(object? obj) @@ -1659,7 +1656,8 @@ public override int GetHashCode() this.interest, (int)this.intersectionRule, (int)this.rasterizationMode, - (int)this.samplingOrigin); + (int)this.samplingOrigin, + this.antialiasThreshold); } private readonly struct EdgePlacement @@ -1746,7 +1744,7 @@ public PreparedCompositeDispatchConfig( /// /// Prepared composite command parameters consumed by . - /// Layout matches the WGSL Params struct exactly (24 u32 fields = 96 bytes). + /// Layout matches the WGSL Params struct exactly (26 u32 fields = 104 bytes). /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeParameters @@ -1775,6 +1773,8 @@ private readonly struct PreparedCompositeParameters public readonly uint SolidG; public readonly uint SolidB; public readonly uint SolidA; + public readonly uint RasterizationMode; + public readonly uint AntialiasThreshold; public PreparedCompositeParameters( int destinationX, @@ -1797,7 +1797,9 @@ public PreparedCompositeParameters( uint colorBlendMode, uint alphaCompositionMode, float blendPercentage, - Vector4 solidColor) + Vector4 solidColor, + uint rasterizationMode, + float antialiasThreshold) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -1823,6 +1825,8 @@ public PreparedCompositeParameters( this.SolidG = FloatToUInt32Bits(solidColor.Y); this.SolidB = FloatToUInt32Bits(solidColor.Z); this.SolidA = FloatToUInt32Bits(solidColor.W); + this.RasterizationMode = rasterizationMode; + this.AntialiasThreshold = FloatToUInt32Bits(antialiasThreshold); } } } diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 2eee14a22..4d1374364 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -50,11 +50,11 @@ - - + + - + diff --git a/src/ImageSharp.Drawing/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs index adfb3c38d..ffc29cce6 100644 --- a/src/ImageSharp.Drawing/PathBuilder.cs +++ b/src/ImageSharp.Drawing/PathBuilder.cs @@ -465,7 +465,7 @@ public PathBuilder Reset() /// /// Clears all drawn paths, Leaving any applied transforms. /// - [MemberNotNull(nameof(this.currentFigure))] + [MemberNotNull(nameof(currentFigure))] public void Clear() { this.currentFigure = new Figure(); @@ -485,7 +485,7 @@ private class Figure public IPath Build() => this.IsClosed - ? new Polygon(this.segments.ToArray(), true) + ? new Polygon([.. this.segments], true) : new Path(this.segments.ToArray()); } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index ef84288ed..2cc8f1f75 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -38,11 +38,6 @@ internal sealed class DefaultDrawingBackend : IDrawingBackend /// public static DefaultDrawingBackend Instance { get; } = new(); - /// - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel - => true; - /// public void FillPath( ICanvasFrame target, diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 2424e2234..2b99e7b64 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -182,6 +182,7 @@ private static void RasterizeCoreRows( maxBandRows, options.IntersectionRule, options.RasterizationMode, + options.AntialiasThreshold, allocator, rowHandler, ref reusableScratch)) @@ -199,6 +200,7 @@ private static void RasterizeCoreRows( maxBandRows, options.IntersectionRule, options.RasterizationMode, + options.AntialiasThreshold, allocator, rowHandler, ref reusableScratch); @@ -216,6 +218,9 @@ private static void RasterizeCoreRows( /// Maximum rows per reusable scratch band. /// Fill rule. /// Coverage mode (AA or aliased). + /// + /// Antialiasing threshold in [0, 1] when is AA. + /// /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. /// @@ -232,6 +237,7 @@ private static void RasterizeSequentialBands( int maxBandRows, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, + float antialiasThreshold, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler, ref WorkerScratch? reusableScratch) @@ -280,7 +286,7 @@ private static void RasterizeSequentialBands( continue; } - Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); + Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode, antialiasThreshold); context.RasterizeEdgeTable(sortedEdges.Slice(start, length), bandTop); context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); context.ResetTouchedRows(); @@ -301,6 +307,9 @@ private static void RasterizeSequentialBands( /// Maximum rows per worker scratch context. /// Fill rule. /// Coverage mode (AA or aliased). + /// + /// Antialiasing threshold in [0, 1] when is AA. + /// /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. /// Caller-managed scratch. Reused when compatible; replaced and updated in place otherwise. @@ -319,6 +328,7 @@ private static bool TryRasterizeParallel( int maxBandRows, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, + float antialiasThreshold, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler, ref WorkerScratch? reusableScratch) @@ -344,6 +354,7 @@ private static bool TryRasterizeParallel( coverStride, intersectionRule, rasterizationMode, + antialiasThreshold, allocator, rowHandler, ref reusableScratch); @@ -398,7 +409,7 @@ private static bool TryRasterizeParallel( if (length > 0) { ReadOnlySpan tileEdges = sortedEdgesMemory.Span.Slice(start, length); - context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode); + context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode, antialiasThreshold); context.RasterizeEdgeTable(tileEdges, bandTop); hasCoverage = true; context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); @@ -435,6 +446,9 @@ private static bool TryRasterizeParallel( /// Cover-area stride in ints. /// Fill rule. /// Coverage mode (AA or aliased). + /// + /// Antialiasing threshold in [0, 1] when is AA. + /// /// Temporary buffer allocator. /// Coverage row callback invoked once per emitted row. /// @@ -450,6 +464,7 @@ private static void RasterizeSingleTileDirect( int coverStride, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, + float antialiasThreshold, MemoryAllocator allocator, RasterizerCoverageRowHandler rowHandler, ref WorkerScratch? reusableScratch) @@ -462,7 +477,7 @@ private static void RasterizeSingleTileDirect( } WorkerScratch scratch = reusableScratch; - Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); + Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode, antialiasThreshold); context.RasterizeEdgeTable(edges, bandTop: 0); context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); context.ResetTouchedRows(); @@ -865,6 +880,7 @@ internal ref struct Context private readonly int coverStride; private readonly IntersectionRule intersectionRule; private readonly RasterizationMode rasterizationMode; + private readonly float antialiasThreshold; private int touchedRowCount; /// @@ -884,7 +900,8 @@ public Context( int wordsPerRow, int coverStride, IntersectionRule intersectionRule, - RasterizationMode rasterizationMode) + RasterizationMode rasterizationMode, + float antialiasThreshold) { this.bitVectors = bitVectors; this.coverArea = coverArea; @@ -900,6 +917,7 @@ public Context( this.coverStride = coverStride; this.intersectionRule = intersectionRule; this.rasterizationMode = rasterizationMode; + this.antialiasThreshold = antialiasThreshold; this.touchedRowCount = 0; } @@ -1252,8 +1270,9 @@ private readonly float AreaToCoverage(int area) if (this.rasterizationMode == RasterizationMode.Aliased) { - // Aliased mode quantizes final coverage to hard 0/1 per pixel. - return coverage >= 0.5F ? 1F : 0F; + // Aliased mode quantizes final coverage to hard 0/1 per pixel + // using the configurable threshold from GraphicsOptions.AntialiasThreshold. + return coverage >= this.antialiasThreshold ? 1F : 0F; } return coverage; @@ -2156,7 +2175,7 @@ public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, i /// /// Creates a context view over this scratch for the requested band height. /// - public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode) + public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, float antialiasThreshold) { if ((uint)bandHeight > (uint)this.tileCapacity) { @@ -2179,7 +2198,8 @@ public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, this.wordsPerRow, this.coverStride, intersectionRule, - rasterizationMode); + rasterizationMode, + antialiasThreshold); } /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 73deea4e8..b8ba054a5 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -14,17 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal interface IDrawingBackend { - /// - /// Determines whether the backend can compose the provided brush type directly for . - /// - /// The destination pixel format. - /// The brush used by a pending composition command. - /// - /// when the backend can compose the brush directly; otherwise . - /// - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel; - /// /// Fills a path into a destination target region. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD index a91abfd57..c97babb31 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD @@ -162,6 +162,22 @@ coverage = min(wrapped, 256) / 256 This is done in `AreaToCoverage(int area)`. +### Aliased Thresholding + +When `RasterizationMode` is `Aliased`, the continuous coverage value computed +above is snapped to binary using `AntialiasThreshold`: + +``` +if coverage >= antialiasThreshold: + coverage = 1.0 +else: + coverage = 0.0 +``` + +Lower threshold values (e.g. 0.1) preserve more edge pixels and thin features; +higher values (e.g. 0.9) produce a tighter, more conservative fill. The default +threshold is 0.5. + ## Why This Handles Self Intersections The scanner does not require geometric boolean normalization first. @@ -183,7 +199,10 @@ rasterization time. - `RasterizerOptions.RasterizationMode` controls whether scanner output is: - `Antialiased`: continuous coverage in `[0, 1]` - - `Aliased`: binary coverage (`0` or `1`), thresholded in the scanner + - `Aliased`: binary coverage (`0` or `1`), thresholded in the scanner using `AntialiasThreshold` +- `RasterizerOptions.AntialiasThreshold` (0–1, default 0.5): the coverage cutoff + used in `Aliased` mode. Pixels with coverage at or above this value become fully + opaque; pixels below are discarded. Ignored in `Antialiased` mode. - `RasterizerSamplingOrigin` affects both X and Y sample alignment (`PixelBoundary` vs `PixelCenter`). ## Data Flow Diagram (Row-Level) diff --git a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 6b627530c..01d1d93b0 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -47,16 +47,19 @@ internal readonly struct RasterizerOptions /// Polygon intersection rule. /// Rasterization coverage mode. /// Sampling origin alignment. + /// Coverage threshold for aliased mode (0–1). public RasterizerOptions( Rectangle interest, IntersectionRule intersectionRule, - RasterizationMode rasterizationMode = RasterizationMode.Antialiased, - RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary) + RasterizationMode rasterizationMode, + RasterizerSamplingOrigin samplingOrigin, + float antialiasThreshold) { this.Interest = interest; this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; this.SamplingOrigin = samplingOrigin; + this.AntialiasThreshold = antialiasThreshold; } /// @@ -79,11 +82,17 @@ public RasterizerOptions( /// public RasterizerSamplingOrigin SamplingOrigin { get; } + /// + /// Gets the coverage threshold used when is . + /// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded. + /// + public float AntialiasThreshold { get; } + /// /// Creates a copy of the current options with a different interest rectangle. /// /// The replacement interest rectangle. /// A new value. public RasterizerOptions WithInterest(Rectangle interest) - => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin); + => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index afb1b8ce9..4edae927c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -846,7 +846,8 @@ private void FillPathCore( interest, shapeOptions.IntersectionRule, rasterizationMode, - samplingOrigin); + samplingOrigin, + graphicsOptions.AntialiasThreshold); this.backend.FillPath( this.targetFrame, @@ -1070,7 +1071,8 @@ private CompositionCommand CreateCompositionCommand( interest, intersectionRule, rasterizationMode, - samplingOrigin); + samplingOrigin, + graphicsOptions.AntialiasThreshold); Point destinationOffset = new( this.targetFrame.Bounds.X + operation.RenderLocation.X, diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs index a0f4f959b..fe29b76e6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -6,7 +6,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Options for influencing the drawing functions. +/// Provides options for influencing drawing operations, combining graphics rendering settings, +/// shape fill-rule behavior, and an optional coordinate transform. /// public class DrawingOptions { @@ -37,7 +38,8 @@ internal DrawingOptions( } /// - /// Gets or sets the Graphics Options. + /// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition, + /// and coverage thresholding for the drawing operation. /// public GraphicsOptions GraphicsOptions { @@ -50,7 +52,7 @@ public GraphicsOptions GraphicsOptions } /// - /// Gets or sets the Shape Options. + /// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior. /// public ShapeOptions ShapeOptions { @@ -63,7 +65,9 @@ public ShapeOptions ShapeOptions } /// - /// Gets or sets the Transform to apply during rasterization. + /// Gets or sets the affine transform matrix applied to vector geometry before rasterization. + /// Can be used to translate, rotate, scale, or skew shapes. + /// Defaults to . /// public Matrix3x2 Transform { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index bba986c04..79e0b6dc2 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -4,7 +4,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Options for influencing the drawing functions. +/// Provides options for controlling how vector shapes are interpreted during rasterization, +/// including the fill-rule intersection mode and boolean clipping operations. /// public class ShapeOptions : IDeepCloneable { @@ -22,14 +23,18 @@ private ShapeOptions(ShapeOptions source) } /// - /// Gets or sets the clipping operation. + /// Gets or sets the boolean clipping operation used when a clipping path is applied. + /// Determines how the clip shape interacts with the target region + /// (e.g. subtracts the clip shape). /// /// Defaults to . /// public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; /// - /// Gets or sets the rule for calculating intersection points. + /// Gets or sets the fill rule that determines how overlapping or nested contours affect coverage. + /// fills any region with a non-zero winding number; + /// alternates fill/hole for each contour crossing. /// /// Defaults to . /// diff --git a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs index 77bd27e49..685fe348c 100644 --- a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs @@ -38,6 +38,14 @@ public void DefaultGraphicsOptionsColorBlendingMode() Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); } + [Fact] + public void DefaultGraphicsOptionsAntialiasThreshold() + { + const float expected = 0.5F; + Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold); + Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold); + } + [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { @@ -53,6 +61,7 @@ public void NonDefaultClone() { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, + AntialiasThreshold = .25F, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -70,6 +79,7 @@ public void CloneIsDeep() actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; + actual.AntialiasThreshold = .25F; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs new file mode 100644 index 000000000..081374ccb --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_134 +{ + [Theory] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, true)] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, false)] + public void LowFontSizeRenderOK(TestImageProvider provider, bool antialias) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.IsWindows) + { + return; + } + + provider.RunValidatingProcessorTest( + c => + { + c.SetGraphicsOptions( + new GraphicsOptions + { + Antialias = antialias, + AntialiasThreshold = .33F + }); + + c.ProcessWithCanvas(canvas => + { + Brush brush = Brushes.Solid(Color.Black); + Font font = SystemFonts.Get("Tahoma").CreateFont(8); + RichTextOptions options = new(font) + { + WrappingLength = c.GetCurrentSize().Width / 2, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + Origin = new PointF(c.GetCurrentSize().Width / 2, c.GetCurrentSize().Height / 2) + }; + + canvas.DrawText(options, "Lorem ipsum dolor sit amet", brush, null); + }); + }, + testOutputDetails: $"{antialias}", + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs index c32ac5ffd..46bf3624f 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. - using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 14e628206..53a84de2e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -26,10 +26,6 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel - => true; - public void FillPath( ICanvasFrame target, IPath path, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index cb62938ec..6637f0895 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -89,6 +89,46 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } + [Theory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.25F } + }; + + EllipsePolygon ellipse = new(256, 256, 200, 150); + Brush brush = Brushes.Solid(Color.Black); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, "FillPath_AliasedThreshold", defaultImage, cpuRegionImage, nativeSurfaceImage); + + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + } + [Theory] [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvider provider) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 12885d426..330dd3ba4 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -37,36 +37,8 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() Assert.Same(brushB, backend.LastBatch.Commands[1].Brush); } - [Fact] - public void Flush_WhenAnyBrushUnsupported_DisablesSharedFlushId() - { - Configuration configuration = new(); - CapturingBackend backend = new() - { - IsBrushSupported = static brush => brush is SolidBrush - }; - configuration.SetDrawingBackend(backend); - - using Image image = new(40, 40); - Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); - - IPath pathA = new RectangularPolygon(2, 2, 12, 12); - IPath pathB = new RectangularPolygon(18, 18, 12, 12); - DrawingOptions options = new(); - using DrawingCanvas canvas = new(configuration, region, options); - - canvas.Fill(pathA, Brushes.Solid(Color.Red)); - canvas.Fill(pathB, Brushes.Horizontal(Color.Blue)); - canvas.Flush(); - - Assert.NotEmpty(backend.Batches); - Assert.All(backend.Batches, static batch => Assert.Equal(0, batch.FlushId)); - } - private sealed class CapturingBackend : IDrawingBackend { - public Func IsBrushSupported { get; init; } = static _ => true; - public List Batches { get; } = []; public bool HasBatch { get; private set; } @@ -79,7 +51,8 @@ private sealed class CapturingBackend : IDrawingBackend Rectangle.Empty, IntersectionRule.NonZero, RasterizationMode.Aliased, - RasterizerSamplingOrigin.PixelBoundary)), + RasterizerSamplingOrigin.PixelBoundary, + 0.5f)), Array.Empty()); public void FillPath( @@ -93,10 +66,6 @@ public void FillPath( => batcher.AddComposition( CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel - => this.IsBrushSupported(brush); - public void FlushCompositions( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index c2a8c5d42..c57085e41 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -162,10 +162,6 @@ public void FillPath( => batcher.AddComposition( CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); - public bool IsCompositionBrushSupported(Brush brush) - where TTargetPixel : unmanaged, IPixel - => true; - public void FlushCompositions( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs new file mode 100644 index 000000000..284fb83f1 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AliasedWithDefaultThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + + int whitePixels = CountPixelsAbove(image, 250); + int partialPixels = CountPixelsBetween(image, 1, 250); + + // Aliased mode should produce no partial-coverage pixels. + Assert.Equal(0, partialPixels); + Assert.True(whitePixels > 0, "Expected some white pixels from the filled circle."); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AliasedLowThreshold_ProducesMorePixelsThanHighThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + + DrawingOptions lowOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.1F } + }; + + DrawingOptions highOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.9F } + }; + + using Image lowImage = provider.GetImage(); + lowImage.Mutate(ctx => ctx.ProcessWithCanvas(lowOptions, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + int lowCount = CountPixelsAbove(lowImage, 250); + + using Image highImage = provider.GetImage(); + highImage.Mutate(ctx => ctx.ProcessWithCanvas(highOptions, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + int highCount = CountPixelsAbove(highImage, 250); + + // A lower threshold includes more edge pixels, so the fill area should be larger. + Assert.True(lowCount > highCount, $"Low threshold ({lowCount} pixels) should produce more pixels than high threshold ({highCount} pixels)."); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AntialiasedIgnoresThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + + DrawingOptions options1 = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.1F } + }; + + DrawingOptions options2 = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.9F } + }; + + using Image image1 = provider.GetImage(); + image1.Mutate(ctx => ctx.ProcessWithCanvas(options1, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + + using Image image2 = provider.GetImage(); + image2.Mutate(ctx => ctx.ProcessWithCanvas(options2, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + + // In antialiased mode the threshold is irrelevant; images should be identical. + ImageComparer.Exact.VerifySimilarity(image1, image2); + } + + private static int CountPixelsAbove(Image image, byte threshold) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R > threshold) + { + count++; + } + } + } + }); + + return count; + } + + private static int CountPixelsBetween(Image image, byte low, byte high) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R >= low && rgba.R < high) + { + count++; + } + } + } + }); + + return count; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 8e0c69900..a5315df2c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -47,10 +47,6 @@ public void SetDrawingBackendOnProcessingContext_RoundTrips() private sealed class RecordingDrawingBackend : IDrawingBackend { - public bool IsCompositionBrushSupported(Brush brush) - where TPixel : unmanaged, IPixel - => true; - public void FillPath( ICanvasFrame target, IPath path, diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs index 2fa41c661..3f3c9af54 100644 --- a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerRegressionTests.cs @@ -15,7 +15,7 @@ public class DefaultRasterizerRegressionTests public void EmitsCoverageForSubpixelThinRectangle() { RectangularPolygon path = new(0.3F, 0.2F, 0.7F, 1.423F); - RasterizerOptions options = new(new Rectangle(0, 0, 12, 20), IntersectionRule.EvenOdd); + RasterizerOptions options = new(new Rectangle(0, 0, 12, 20), IntersectionRule.EvenOdd, RasterizationMode.Antialiased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); float[] coverage = new float[options.Interest.Width * options.Interest.Height]; int width = options.Interest.Width; int top = options.Interest.Top; @@ -47,7 +47,7 @@ void CaptureRow(int y, int startX, Span rowCoverage) public void RasterizesFractionalRectangleCoverageDeterministically() { RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); - RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero); + RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero, RasterizationMode.Antialiased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); float[] coverage = Rasterize(path, options); float[] expected = @@ -66,7 +66,7 @@ public void RasterizesFractionalRectangleCoverageDeterministically() public void AliasedMode_EmitsBinaryCoverage() { RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); - RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero, RasterizationMode.Aliased); + RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero, RasterizationMode.Aliased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); float[] coverage = Rasterize(path, options); float[] expected = @@ -82,7 +82,7 @@ public void AliasedMode_EmitsBinaryCoverage() public void ThrowsForInterestTooWideForCoverStrideMath() { RectangularPolygon path = new(0F, 0F, 1F, 1F); - RasterizerOptions options = new(new Rectangle(0, 0, (int.MaxValue / 2) + 1, 1), IntersectionRule.NonZero); + RasterizerOptions options = new(new Rectangle(0, 0, (int.MaxValue / 2) + 1, 1), IntersectionRule.NonZero, RasterizationMode.Antialiased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); void Rasterize() => DefaultRasterizer.RasterizeRows( diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs index f8c6e35b2..383927e1e 100644 --- a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs @@ -32,7 +32,7 @@ public void MatchesDefaultRasterizer_ForLargeSelfIntersectingPath(IntersectionRu .Transform(Matrix3x2.CreateScale(200F)); Rectangle interest = Rectangle.Ceiling(path.Bounds); - RasterizerOptions options = new(interest, rule); + RasterizerOptions options = new(interest, rule, RasterizationMode.Antialiased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); float[] expected = RasterizeSequential(path, options); float[] actual = Rasterize(path, options); @@ -48,7 +48,9 @@ public void MatchesDefaultRasterizer_ForPixelCenterSampling() RasterizerOptions options = new( interest, IntersectionRule.NonZero, - samplingOrigin: RasterizerSamplingOrigin.PixelCenter); + RasterizationMode.Antialiased, + RasterizerSamplingOrigin.PixelCenter, + 0.5f); float[] expected = RasterizeSequential(path, options); float[] actual = Rasterize(path, options); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs index fcdb3f9c2..c58fafc40 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -16,6 +16,7 @@ public bool Equals(GraphicsOptions x, GraphicsOptions y) return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias + && x.AntialiasThreshold == y.AntialiasThreshold && x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode; } diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png new file mode 100644 index 000000000..adef79dcd --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67515a55bc0bed27054d344d430e0ea0ab0ff15a5038b6b33e8f6118f2d84d42 +size 176 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png new file mode 100644 index 000000000..c2628e2ad --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19039af8089b9ba3544553e7bd1248c135a8d9c17bdfc46728093bff7eef4bbf +size 675 From a96c74c3049797a7c6c4eb1da2b35b9957a0b82a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 13:50:08 +1000 Subject: [PATCH 102/136] Remove FillPath API from drawing backends --- .../WebGPUDrawingBackend.cs | 20 ---------------- .../Backends/DefaultDrawingBackend.cs | 12 ---------- .../Processing/Backends/IDrawingBackend.cs | 19 --------------- .../Processing/DrawingCanvas{TPixel}.cs | 24 +++++++++---------- .../Backends/SkiaCoverageDrawingBackend.cs | 16 ------------- .../Processing/DrawingCanvasBatcherTests.cs | 11 --------- .../Processing/DrawingCanvasTests.Process.cs | 11 --------- .../RasterizerDefaultsExtensionsTests.cs | 11 --------- 8 files changed, 12 insertions(+), 112 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 6601c41bd..c6a7099d6 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -142,26 +142,6 @@ internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) return false; } - /// - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel - { - this.ThrowIfDisposed(); - batcher.AddComposition( - CompositionCommand.Create( - path, - brush, - graphicsOptions, - rasterizerOptions, - target.Bounds.Location)); - } - /// public void FlushCompositions( Configuration configuration, diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 2cc8f1f75..3c536e116 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -38,18 +38,6 @@ internal sealed class DefaultDrawingBackend : IDrawingBackend /// public static DefaultDrawingBackend Instance { get; } = new(); - /// - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel - => batcher.AddComposition( - CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); - /// public void FlushCompositions( Configuration configuration, diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index b8ba054a5..13cbf52ae 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -14,25 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal interface IDrawingBackend { - /// - /// Fills a path into a destination target region. - /// - /// The pixel format. - /// Destination frame. - /// Path in target-local coordinates. - /// Brush used to shade covered pixels. - /// Graphics blending/composition options. - /// Rasterizer options in target-local coordinates. - /// Batcher used to queue normalized composition commands. - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel; - /// /// Flushes queued composition operations for the target. /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 4edae927c..02b992e4d 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -351,7 +351,7 @@ public void Fill(IPath path, Brush brush) transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); - this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); + this.PrepareCompositionCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); } /// @@ -407,7 +407,7 @@ public void Process(IPath path, Action operation) ImageBrush brush = new(sourceImage, sourceImage.Bounds, brushOffset); this.pendingImageResources.Add(sourceImage); - this.FillPathCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); + this.PrepareCompositionCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); } /// @@ -480,7 +480,7 @@ public void Draw(Pen pen, IPath path) outline = ApplyClipPaths(outline, effectiveOptions.ShapeOptions, state.ClipPaths); - this.FillPathCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); + this.PrepareCompositionCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); } /// @@ -813,13 +813,13 @@ private void DrawImageCore( } /// - /// Rasterizes and submits a fill operation to the backend. + /// Prepares a path fill composition command and enqueues it in the batcher. /// /// Path to fill. /// Brush used for shading. /// Effective drawing options. /// Rasterizer sampling origin. - private void FillPathCore( + private void PrepareCompositionCore( IPath path, Brush brush, DrawingOptions options, @@ -849,13 +849,13 @@ private void FillPathCore( samplingOrigin, graphicsOptions.AntialiasThreshold); - this.backend.FillPath( - this.targetFrame, - path, - brush, - graphicsOptions, - rasterizerOptions, - this.batcher); + this.batcher.AddComposition( + CompositionCommand.Create( + path, + brush, + graphicsOptions, + rasterizerOptions, + this.targetFrame.Bounds.Location)); } /// diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs index 53a84de2e..4abe103e3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs @@ -26,22 +26,6 @@ internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable public int LiveCoverageCount => this.preparedCoverage.Count; - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel - => batcher.AddComposition( - CompositionCommand.Create( - path, - brush, - graphicsOptions, - rasterizerOptions, - target.Bounds.Location)); - public void FlushCompositions( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 330dd3ba4..3ef419f0d 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -55,17 +55,6 @@ private sealed class CapturingBackend : IDrawingBackend 0.5f)), Array.Empty()); - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel - => batcher.AddComposition( - CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); - public void FlushCompositions( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index c57085e41..a22a960df 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -151,17 +151,6 @@ public MirroringCpuReadbackTestBackend(ICanvasFrame proxyFrame, Image( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TTargetPixel : unmanaged, IPixel - => batcher.AddComposition( - CompositionCommand.Create(path, brush, graphicsOptions, rasterizerOptions, target.Bounds.Location)); - public void FlushCompositions( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index a5315df2c..d58e2cdfa 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -47,17 +47,6 @@ public void SetDrawingBackendOnProcessingContext_RoundTrips() private sealed class RecordingDrawingBackend : IDrawingBackend { - public void FillPath( - ICanvasFrame target, - IPath path, - Brush brush, - GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - DrawingCanvasBatcher batcher) - where TPixel : unmanaged, IPixel - { - } - public void FlushCompositions( Configuration configuration, ICanvasFrame target, From 23c5c378e0a6bf387cfb3acb705826aa111a791c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 15:51:48 +1000 Subject: [PATCH 103/136] Add WebGPUWindowDemo and backend feature checks --- ImageSharp.Drawing.sln | 59 ++ samples/DrawShapesWithImageSharp/Program.cs | 8 +- samples/DrawShapesWithImageSharp/README.md | 21 + samples/WebGPUWindowDemo/Program.cs | 509 ++++++++++++++++++ samples/WebGPUWindowDemo/README.md | 46 ++ .../WebGPUWindowDemo/WebGPUWindowDemo.csproj | 36 ++ src/Directory.Build.props | 1 + .../WebGPUDrawingBackend.CompositePixels.cs | 60 ++- .../WebGPUFlushContext.cs | 39 +- .../WebGPURuntime.cs | 70 +++ 10 files changed, 825 insertions(+), 24 deletions(-) create mode 100644 samples/DrawShapesWithImageSharp/README.md create mode 100644 samples/WebGPUWindowDemo/Program.cs create mode 100644 samples/WebGPUWindowDemo/README.md create mode 100644 samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index c7e333c09..e34ac25a0 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -339,32 +339,90 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{2541FDCD-78AC-40DB-B5E3-6A715DC132BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x64.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x86.Build.0 = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.ActiveCfg = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.Build.0 = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.ActiveCfg = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.Build.0 = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.Build.0 = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.Build.0 = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.ActiveCfg = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.Build.0 = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.ActiveCfg = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.Build.0 = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.ActiveCfg = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.Build.0 = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.ActiveCfg = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.Build.0 = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.ActiveCfg = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.Build.0 = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.ActiveCfg = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.Build.0 = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x64.Build.0 = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x86.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x64.ActiveCfg = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x64.Build.0 = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x86.ActiveCfg = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x86.Build.0 = Release|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.Build.0 = Debug|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x64.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x86.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|Any CPU.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x64.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x64.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x86.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -393,6 +451,7 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index d5fd9309e..4cf0ecade 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -111,7 +111,7 @@ private static void DrawSerializedOPenSansLetterShape_a() })]; ComplexPolygon complex = new(polys); - complex.SaveImage("letter", "a.png"); + complex.SaveImage("Letter", "a.png"); } private static void DrawSerializedOPenSansLetterShape_o() @@ -129,7 +129,7 @@ private static void DrawSerializedOPenSansLetterShape_o() })]; ComplexPolygon complex = new(polys); - complex.SaveImage("letter", "o.png"); + complex.SaveImage("Letter", "o.png"); } private static void DrawOval() @@ -157,7 +157,7 @@ private static void OutputDrawnShape() sb.AddLine(new Vector2(25, 30), new Vector2(15, 30)); sb.CloseFigure(); - sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"paths.png"); + sb.Build().Translate(0, 10).Scale(10).SaveImage("Drawing", $"paths.png"); } private static void OutputDrawnShapeHourGlass() @@ -174,7 +174,7 @@ private static void OutputDrawnShapeHourGlass() sb.AddLine(new Vector2(15, 30), new Vector2(25, 30)); sb.CloseFigure(); - sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"HourGlass.png"); + sb.Build().Translate(0, 10).Scale(10).SaveImage("Drawing", $"HourGlass.png"); } private static void OutputStarOutline(int points, float inner = 10, float outer = 20, float width = 5, LineJoin jointStyle = LineJoin.Miter) diff --git a/samples/DrawShapesWithImageSharp/README.md b/samples/DrawShapesWithImageSharp/README.md new file mode 100644 index 000000000..1d1bd9c82 --- /dev/null +++ b/samples/DrawShapesWithImageSharp/README.md @@ -0,0 +1,21 @@ +# Draw Shapes With ImageSharp + +A sample application that demonstrates the core vector drawing capabilities of ImageSharp.Drawing. Each example renders shapes to an `Image` using the `DrawingCanvas` API and saves the result as a PNG file in the `Output/` directory. + +## What it demonstrates + +- **Stars** — Filled and outlined star polygons with varying point counts, inner/outer radii, line join styles (miter, round, bevel), and dashed outlines with different line caps. +- **Clipping** — Rectangle-on-rectangle clipping using `IPath.Clip()`. +- **Path building** — Constructing complex shapes with `PathBuilder`, including multi-figure paths (a V overlaid with a rectangle) and an hourglass shape. +- **Curves** — Ellipses via `EllipsePolygon` and cubic Bezier arcs via `CubicBezierLineSegment`. +- **Text as paths** — Converting text to vector outlines using `TextBuilder.GeneratePaths()` with system fonts, including text laid out along a curved path. +- **Serialized glyph data** — Rendering OpenSans letter shapes ('a' and 'o') from serialized coordinate data as `ComplexPolygon` instances. +- **Canvas API** — `Fill` for solid backgrounds and shape rendering via `ProcessWithCanvas`. + +## Running + +```bash +dotnet run --project samples/DrawShapesWithImageSharp -c Debug +``` + +Output images are written to the `Output/` directory, organized into subdirectories by category: `Stars/`, `Clipping/`, `Curves/`, `Text/`, `Drawing/`, `Letter/`, and `Issues/`. diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs new file mode 100644 index 000000000..1dcbbbcda --- /dev/null +++ b/samples/WebGPUWindowDemo/Program.cs @@ -0,0 +1,509 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using Silk.NET.Core.Native; +using Silk.NET.Maths; +using Silk.NET.WebGPU; +using Silk.NET.Windowing; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using Color = SixLabors.ImageSharp.Color; +using Rectangle = SixLabors.ImageSharp.Rectangle; + +namespace WebGPUWindowDemo; + +/// +/// Demonstrates the ImageSharp.Drawing WebGPU backend rendering directly to a native +/// swap chain surface. Bouncing ellipses and vertically scrolling text are composited +/// each frame using the API backed by a WebGPU +/// compute compositor. +/// +public static unsafe class Program +{ + private const int WindowWidth = 800; + private const int WindowHeight = 600; + private const int BallCount = 10; + + // Silk.NET WebGPU API and windowing handles. + private static WebGPU wgpu; + private static IWindow window; + + // WebGPU device-level handles. + private static Instance* instance; + private static Surface* surface; + private static SurfaceConfiguration surfaceConfiguration; + private static SurfaceCapabilities surfaceCapabilities; + private static Adapter* adapter; + private static Device* device; + private static Queue* queue; + + // ImageSharp.Drawing backend and configuration. + private static WebGPUDrawingBackend backend; + private static Configuration drawingConfiguration; + + // Bouncing ball simulation state. + private static Ball[] balls; + private static readonly Random Rng = new(42); + + // FPS counter state. + private static int frameCount; + private static double fpsElapsed; + + // Scrolling text state — glyph geometry is built once at startup via TextBuilder + // and translated vertically each frame. Only glyphs whose bounds intersect the + // visible viewport are submitted for rasterization. + private static IPathCollection scrollPaths = null!; + private static float scrollOffset; + private static float scrollTextHeight; + private const string ScrollText = + "ImageSharp.Drawing on WebGPU\n\n" + + "Real-time GPU-accelerated 2D vector graphics " + + "rendered directly to a native swap chain surface.\n\n" + + "The canvas API provides a familiar drawing model: " + + "Fill, Draw, DrawText, Clip, and Transform — " + + "all composited on the GPU via compute shaders.\n\n" + + "Text is shaped once using SixLabors.Fonts and " + + "converted to vector paths via TextBuilder. " + + "Each frame simply translates the cached geometry.\n\n" + + "Shapes are rasterized into coverage masks on the " + + "CPU, uploaded to GPU textures, then composited " + + "using a WebGPU compute pipeline that evaluates " + + "Porter-Duff blending per pixel.\n\n" + + "The drawing backend automatically manages texture " + + "atlases, bind groups, and pipeline state. It falls " + + "back to the CPU backend for unsupported pixel " + + "formats or when no GPU device is available.\n\n" + + "SixLabors ImageSharp.Drawing\n" + + "github.com/SixLabors/ImageSharp.Drawing\n\n" + + "Built with Silk.NET WebGPU bindings.\n" + + "Running on your GPU right now."; + + public static void Main() + { + // Create a window with no built-in graphics API — we manage WebGPU ourselves. + WindowOptions options = WindowOptions.Default; + options.API = GraphicsAPI.None; + options.Size = new Vector2D(WindowWidth, WindowHeight); + options.Title = "ImageSharp.Drawing WebGPU Demo"; + options.ShouldSwapAutomatically = false; + options.IsContextControlDisabled = true; + + window = Window.Create(options); + window.Load += OnLoad; + window.Update += OnUpdate; + window.Render += OnRender; + window.Closing += OnClosing; + window.FramebufferResize += OnFramebufferResize; + + window.Run(); + } + + /// + /// Called once when the window is ready. Bootstraps the WebGPU device, configures + /// the swap chain, initializes the ImageSharp.Drawing backend, pre-builds the + /// scrolling text geometry, and seeds the ball simulation. + /// + private static void OnLoad() + { + // Bootstrap WebGPU: instance → surface → adapter → device → queue. + wgpu = WebGPU.GetApi(); + + InstanceDescriptor instanceDescriptor = default; + instance = wgpu.CreateInstance(&instanceDescriptor); + + surface = window.CreateWebGPUSurface(wgpu, instance); + + // Request an adapter compatible with our window surface. + RequestAdapterOptions adapterOptions = new() + { + CompatibleSurface = surface + }; + + wgpu.InstanceRequestAdapter( + instance, + ref adapterOptions, + new PfnRequestAdapterCallback((_, a, _, _) => adapter = a), + null); + + Console.WriteLine($"Adapter: 0x{(nuint)adapter:X}"); + + // Request a device with Bgra8UnormStorage — required by the compute compositor + // to write storage textures in Bgra8Unorm format (the swap chain format). + FeatureName requiredFeature = FeatureName.Bgra8UnormStorage; + DeviceDescriptor deviceDescriptor = new() + { + DeviceLostCallback = new PfnDeviceLostCallback(DeviceLost), + RequiredFeatureCount = 1, + RequiredFeatures = &requiredFeature, + }; + + wgpu.AdapterRequestDevice( + adapter, + in deviceDescriptor, + new PfnRequestDeviceCallback((_, d, _, _) => device = d), + null); + + wgpu.DeviceSetUncapturedErrorCallback(device, new PfnErrorCallback(UncapturedError), null); + + queue = wgpu.DeviceGetQueue(device); + + Console.WriteLine($"Device: 0x{(nuint)device:X}, Queue: 0x{(nuint)queue:X}"); + + // Register shared handles so the drawing backend uses our device rather than + // provisioning its own. + WebGPURuntime.SetSharedHandles((nint)device, (nint)queue); + + // Query surface capabilities and configure the swap chain. + wgpu.SurfaceGetCapabilities(surface, adapter, ref surfaceCapabilities); + Console.WriteLine($"Surface format: {surfaceCapabilities.Formats[0]}"); + ConfigureSwapchain(); + + // Initialize the ImageSharp.Drawing WebGPU backend and attach it to a + // cloned Configuration so it doesn't affect the global default. + backend = new WebGPUDrawingBackend(); + drawingConfiguration = Configuration.Default.Clone(); + drawingConfiguration.SetDrawingBackend(backend); + + // Pre-build scrolling text geometry at the origin. TextBuilder converts the + // shaped text into an IPathCollection of glyph outlines that can be cheaply + // translated each frame without re-shaping or re-building outlines. + Font scrollFont = SystemFonts.CreateFont("Arial", 24); + TextOptions textOptions = new(scrollFont) + { + Origin = new Vector2(WindowWidth / 2f, 0), + WrappingLength = WindowWidth - 80, + HorizontalAlignment = HorizontalAlignment.Center, + LineSpacing = 1.6f, + }; + + scrollPaths = TextBuilder.GeneratePaths(ScrollText, textOptions); + FontRectangle bounds = TextMeasurer.MeasureSize(ScrollText, textOptions); + scrollTextHeight = bounds.Height; + + // Seed the bouncing ball simulation with random positions, velocities, and colors. + balls = new Ball[BallCount]; + for (int i = 0; i < BallCount; i++) + { + balls[i] = Ball.CreateRandom(Rng, WindowWidth, WindowHeight); + } + + Console.WriteLine("WebGPU windowed demo initialized."); + } + + /// + /// Configures (or reconfigures) the swap chain for the current framebuffer size. + /// Uses Bgra8Unorm to match the canvas pixel format. + /// CopyDst is required because the compositor copies from a transient output texture + /// into the swap chain target. TextureBinding is needed for backdrop sampling. + /// + private static void ConfigureSwapchain() + { + surfaceConfiguration = new SurfaceConfiguration + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopyDst | TextureUsage.TextureBinding, + Format = TextureFormat.Bgra8Unorm, + PresentMode = PresentMode.Fifo, + Device = device, + Width = (uint)window.FramebufferSize.X, + Height = (uint)window.FramebufferSize.Y, + }; + + wgpu.SurfaceConfigure(surface, ref surfaceConfiguration); + } + + /// + /// Reconfigures the swap chain when the window is resized. + /// + private static void OnFramebufferResize(Vector2D size) + { + if (size.X > 0 && size.Y > 0) + { + ConfigureSwapchain(); + } + } + + /// + /// Fixed-timestep update: advances ball positions and the scroll offset. + /// + private static void OnUpdate(double deltaTime) + { + int w = window.FramebufferSize.X; + int h = window.FramebufferSize.Y; + float dt = (float)deltaTime; + + // Integrate ball positions and bounce off walls. + for (int i = 0; i < balls.Length; i++) + { + balls[i].Update(dt, w, h); + } + + // Advance scrolling text vertically (pixels per second). + scrollOffset += 200f * dt; + } + + /// + /// Per-frame render callback. Acquires a swap chain texture, wraps it as a + /// , creates a , + /// draws all content, flushes the GPU composition, and presents. + /// + private static void OnRender(double deltaTime) + { + int w = window.FramebufferSize.X; + int h = window.FramebufferSize.Y; + if (w <= 0 || h <= 0) + { + return; + } + + // Acquire the next swap chain texture from the surface. + SurfaceTexture surfaceTexture; + wgpu.SurfaceGetCurrentTexture(surface, &surfaceTexture); + switch (surfaceTexture.Status) + { + case SurfaceGetCurrentTextureStatus.Timeout: + case SurfaceGetCurrentTextureStatus.Outdated: + case SurfaceGetCurrentTextureStatus.Lost: + wgpu.TextureRelease(surfaceTexture.Texture); + ConfigureSwapchain(); + return; + case SurfaceGetCurrentTextureStatus.OutOfMemory: + case SurfaceGetCurrentTextureStatus.DeviceLost: + throw new InvalidOperationException($"Surface texture error: {surfaceTexture.Status}"); + } + + TextureView* textureView = wgpu.TextureCreateView(surfaceTexture.Texture, null); + + try + { + // Wrap the swap chain texture as a NativeSurface so the drawing backend + // can composite directly into it. The format must match the swap chain + // configuration (Bgra8Unorm) and the canvas pixel type (Bgra32). + NativeSurface nativeSurface = WebGPUNativeSurfaceFactory.Create( + (nint)device, + (nint)queue, + (nint)surfaceTexture.Texture, + (nint)textureView, + WebGPUTextureFormatId.Bgra8Unorm, + w, + h, + isSrgb: false, + isPremultipliedAlpha: false, + supportsTextureSampling: true); + + // NativeSurfaceOnlyFrame exposes only the GPU surface (no CPU region), + // so the backend always takes the GPU composition path. + NativeSurfaceOnlyFrame frame = new(new Rectangle(0, 0, w, h), nativeSurface); + + // Create a drawing canvas targeting the swap chain frame. + using DrawingCanvas canvas = new(drawingConfiguration, frame, new DrawingOptions()); + + // Clear to a dark background. + canvas.Fill(Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255)))); + + // Draw vertically scrolling text behind the balls. + DrawScrollingText(canvas, w, h); + + // Draw each ball as a filled ellipse. + for (int i = 0; i < balls.Length; i++) + { + ref Ball ball = ref balls[i]; + EllipsePolygon ellipse = new(ball.X, ball.Y, ball.Radius); + canvas.Fill(ellipse, Brushes.Solid(ball.Color)); + } + + // Flush submits all queued draw operations to the GPU compositor and + // copies the composited result into the swap chain texture. + canvas.Flush(); + } + finally + { + // Present the frame and release per-frame WebGPU resources. + wgpu.SurfacePresent(surface); + wgpu.TextureViewRelease(textureView); + wgpu.TextureRelease(surfaceTexture.Texture); + } + + // Update FPS counter in the window title once per second. + frameCount++; + fpsElapsed += deltaTime; + if (fpsElapsed >= 1.0) + { + window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS"; + frameCount = 0; + fpsElapsed = 0; + } + } + + /// + /// Draws the pre-built scrolling text geometry. The full text block scrolls upward + /// and loops when it passes above the window. Each glyph path is bounds-tested + /// against the viewport so only visible glyphs are rasterized. + /// + private static void DrawScrollingText(DrawingCanvas canvas, int w, int h) + { + if (scrollTextHeight <= 0) + { + return; + } + + // Total cycle: text enters from the bottom, scrolls up, exits the top, then loops. + float totalCycle = h + scrollTextHeight; + float wrappedOffset = scrollOffset % totalCycle; + float y = h - wrappedOffset; + + Matrix3x2 translation = Matrix3x2.CreateTranslation(0, y); + RectangleF viewport = new(0, 0, w, h); + Brush textBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255))); + + // Each IPath in scrollPaths is one glyph outline. Skip any whose translated + // bounding box doesn't intersect the visible area. + foreach (IPath path in scrollPaths) + { + RectangleF pathBounds = path.Bounds; + RectangleF translated = new( + pathBounds.X + translation.M31, + pathBounds.Y + translation.M32, + pathBounds.Width, + pathBounds.Height); + + if (!viewport.IntersectsWith(translated)) + { + continue; + } + + canvas.Fill(path.Transform(translation), textBrush); + } + } + + /// + /// Tears down the drawing backend and releases all WebGPU resources in reverse + /// creation order. + /// + private static void OnClosing() + { + backend.Dispose(); + WebGPURuntime.ClearSharedHandles(); + + wgpu.DeviceRelease(device); + wgpu.AdapterRelease(adapter); + wgpu.SurfaceRelease(surface); + wgpu.InstanceRelease(instance); + wgpu.Dispose(); + + WebGPURuntime.Shutdown(); + } + + /// WebGPU device-lost callback — logs the reason to the console. + private static void DeviceLost(DeviceLostReason reason, byte* message, void* userData) + => Console.WriteLine($"Device lost ({reason}): {SilkMarshal.PtrToString((nint)message)}"); + + /// WebGPU uncaptured error callback — logs validation errors to the console. + private static void UncapturedError(ErrorType type, byte* message, void* userData) + => Console.WriteLine($"WebGPU {type}: {SilkMarshal.PtrToString((nint)message)}"); + + /// + /// Wraps a as an that + /// exposes only the GPU surface. returns false, ensuring + /// the drawing backend always takes the native GPU composition path. + /// + private sealed class NativeSurfaceOnlyFrame : ICanvasFrame + where TPixel : unmanaged, IPixel + { + private readonly NativeSurface nativeSurface; + + public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface nativeSurface) + { + this.Bounds = bounds; + this.nativeSurface = nativeSurface; + } + + public Rectangle Bounds { get; } + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + public bool TryGetNativeSurface(out NativeSurface surface) + { + surface = this.nativeSurface; + return true; + } + } + + /// + /// A simple bouncing ball with position, velocity, radius, and color. + /// Reflects off the window edges each frame. + /// + private struct Ball + { + public float X; + public float Y; + public float VelocityX; + public float VelocityY; + public float Radius; + public Color Color; + + /// + /// Creates a ball with a random position inside the window bounds, a random + /// velocity between 100-300 px/s in each axis, a random radius between 20-60 px, + /// and a random semi-transparent color. + /// + public static Ball CreateRandom(Random rng, int width, int height) + { + float radius = 20f + (rng.NextSingle() * 40f); + return new Ball + { + X = radius + (rng.NextSingle() * (width - (2 * radius))), + Y = radius + (rng.NextSingle() * (height - (2 * radius))), + VelocityX = (100f + (rng.NextSingle() * 200f)) * (rng.Next(2) == 0 ? -1 : 1), + VelocityY = (100f + (rng.NextSingle() * 200f)) * (rng.Next(2) == 0 ? -1 : 1), + Radius = radius, + Color = Color.FromPixel(new Bgra32( + (byte)(80 + rng.Next(176)), + (byte)(80 + rng.Next(176)), + (byte)(80 + rng.Next(176)), + 200)), + }; + } + + /// + /// Integrates position by velocity and reflects off the window edges. + /// + public void Update(float dt, int width, int height) + { + this.X += this.VelocityX * dt; + this.Y += this.VelocityY * dt; + + if (this.X - this.Radius < 0) + { + this.X = this.Radius; + this.VelocityX = MathF.Abs(this.VelocityX); + } + else if (this.X + this.Radius > width) + { + this.X = width - this.Radius; + this.VelocityX = -MathF.Abs(this.VelocityX); + } + + if (this.Y - this.Radius < 0) + { + this.Y = this.Radius; + this.VelocityY = MathF.Abs(this.VelocityY); + } + else if (this.Y + this.Radius > height) + { + this.Y = height - this.Radius; + this.VelocityY = -MathF.Abs(this.VelocityY); + } + } + } +} diff --git a/samples/WebGPUWindowDemo/README.md b/samples/WebGPUWindowDemo/README.md new file mode 100644 index 000000000..a49b25c68 --- /dev/null +++ b/samples/WebGPUWindowDemo/README.md @@ -0,0 +1,46 @@ +# WebGPU Window Demo + +A real-time sample application that renders directly to a native window swap chain using the ImageSharp.Drawing WebGPU backend. Bouncing ellipses and vertically scrolling text are composited each frame via the `DrawingCanvas` API, with all composition performed by a WebGPU compute pipeline. + +## What it demonstrates + +- **Native surface rendering** — The swap chain texture is wrapped as a `NativeSurface` and passed to the canvas via `ICanvasFrame`. The backend composites directly into the swap chain target without CPU readback. +- **WebGPU bootstrap** — Full Silk.NET WebGPU initialization: instance → surface → adapter → device → queue, with `Bgra8UnormStorage` requested for compute storage writes to `Bgra8Unorm` textures. +- **Pre-built text geometry** — `TextBuilder.GeneratePaths` shapes the text once at startup. Each frame translates the cached `IPathCollection` with a `Matrix3x2` — no re-shaping or re-building of glyph outlines. +- **Viewport culling** — Only glyph paths whose translated bounding boxes intersect the visible window are submitted for rasterization, keeping frame times low even with large text blocks. +- **Canvas API** — `Fill` for solid backgrounds, `Fill(IPath, Brush)` for ellipses and glyph outlines, `Flush` to submit all queued operations to the GPU compositor. + +## Prerequisites + +- .NET 8.0 SDK or later +- A GPU with Vulkan, Metal, or D3D12 support +- The adapter must support the `Bgra8UnormStorage` feature (most desktop GPUs do) + +## Running + +```bash +dotnet run --project samples/WebGPUWindowDemo -c Debug +``` + +An 800×600 window opens showing colored ellipses bouncing off the walls with scrolling descriptive text in the background. The window title displays the current FPS. + +## Architecture + +Each frame follows this sequence: + +1. `SurfaceGetCurrentTexture` — acquire the next swap chain texture +2. `WebGPUNativeSurfaceFactory.Create(...)` — wrap it as a `NativeSurface` +3. `NativeSurfaceOnlyFrame` — wrap as `ICanvasFrame` (GPU path only, no CPU region) +4. `new DrawingCanvas(config, frame, options)` — create the canvas +5. `canvas.Fill(...)` — queue background, text glyphs, and ellipses +6. `canvas.Flush()` — rasterize coverage masks on CPU, upload to GPU, composite via compute shader, copy result to swap chain texture +7. `SurfacePresent()` — present the frame + +## Performance + +| Scenario | FPS (typical) | +|---|---| +| Balls only (no text) | ~120 | +| Balls + scrolling text | 65–120 | + +FPS varies with how much text is currently visible in the viewport. diff --git a/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj b/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj new file mode 100644 index 000000000..7869e86da --- /dev/null +++ b/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj @@ -0,0 +1,36 @@ + + + + portable + Exe + true + + + + + + net8.0;net10.0 + + + + + net8.0 + + + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5ac8f2bf0..d833a7ac3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -30,6 +30,7 @@ + diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index 74724186b..1b2e59e6d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -14,6 +14,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// The map defined by is intentionally explicit and only /// includes one-to-one format mappings where the GPU texture format can round-trip the pixel payload /// without channel swizzle or custom conversion logic. +/// Only formats that support storage texture binding (required by the compute compositor) +/// are included. Formats that lack storage support are omitted and fall back to the CPU backend. /// internal sealed partial class WebGPUDrawingBackend { @@ -24,37 +26,29 @@ internal sealed partial class WebGPUDrawingBackend /// The registration map used during flush dispatch. private static Dictionary CreateCompositePixelHandlers() => - // No-swizzle mappings only. Unsupported types are intentionally omitted from this map. + // Only formats with native or feature-gated storage binding support. + // Non-storable formats (R8Unorm, RG8Unorm, RG8Snorm, R16Float, RG16Float, + // RG16Sint, Rgb10A2Unorm, R16Uint, RG16Uint) are omitted — they cannot be + // used as compute shader write targets and fall back to DefaultDrawingBackend. new() { - [typeof(A8)] = CompositePixelRegistration.Create(TextureFormat.R8Unorm), - [typeof(L8)] = CompositePixelRegistration.Create(TextureFormat.R8Unorm), - [typeof(La16)] = CompositePixelRegistration.Create(TextureFormat.RG8Unorm), - [typeof(Byte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Uint), - [typeof(NormalizedByte2)] = CompositePixelRegistration.Create(TextureFormat.RG8Snorm), [typeof(NormalizedByte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Snorm), - [typeof(HalfSingle)] = CompositePixelRegistration.Create(TextureFormat.R16float), - [typeof(HalfVector2)] = CompositePixelRegistration.Create(TextureFormat.RG16float), [typeof(HalfVector4)] = CompositePixelRegistration.Create(TextureFormat.Rgba16float), - [typeof(Short2)] = CompositePixelRegistration.Create(TextureFormat.RG16Sint), [typeof(Short4)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Sint), - [typeof(Rgba1010102)] = CompositePixelRegistration.Create(TextureFormat.Rgb10A2Unorm), [typeof(Rgba32)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Unorm), - [typeof(Bgra32)] = CompositePixelRegistration.Create(TextureFormat.Bgra8Unorm), + [typeof(Bgra32)] = CompositePixelRegistration.Create(TextureFormat.Bgra8Unorm, FeatureName.Bgra8UnormStorage), [typeof(RgbaVector)] = CompositePixelRegistration.Create(TextureFormat.Rgba32float), - [typeof(L16)] = CompositePixelRegistration.Create(TextureFormat.R16Uint), - [typeof(La32)] = CompositePixelRegistration.Create(TextureFormat.RG16Uint), - [typeof(Rg32)] = CompositePixelRegistration.Create(TextureFormat.RG16Uint), [typeof(Rgba64)] = CompositePixelRegistration.Create(TextureFormat.Rgba16Uint) }; /// - /// Resolves the WebGPU texture format identifier for when supported. + /// Resolves the WebGPU texture format identifier for when supported + /// by the current device. /// /// The requested pixel type. /// Receives the mapped texture format identifier on success. @@ -71,6 +65,13 @@ internal static bool TryGetCompositeTextureFormat(out WebGPUTextureForma return false; } + if (registration.RequiredFeature != FeatureName.Undefined + && !WebGPURuntime.HasDeviceFeature(registration.RequiredFeature)) + { + formatId = default; + return false; + } + formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); return true; } @@ -86,11 +87,17 @@ private readonly struct CompositePixelRegistration /// The registered pixel CLR type. /// The matching WebGPU texture format. /// The unmanaged pixel size in bytes. - public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, int pixelSizeInBytes) + /// Optional device feature required for storage binding support. + public CompositePixelRegistration( + Type pixelType, + TextureFormat textureFormat, + int pixelSizeInBytes, + FeatureName requiredFeature) { this.PixelType = pixelType; this.TextureFormat = textureFormat; this.PixelSizeInBytes = pixelSizeInBytes; + this.RequiredFeature = requiredFeature; } /// @@ -109,13 +116,30 @@ public CompositePixelRegistration(Type pixelType, TextureFormat textureFormat, i public int PixelSizeInBytes { get; } /// - /// Creates a registration record for . + /// Gets the optional device feature required for storage binding support. + /// means the format is natively storable. + /// + public FeatureName RequiredFeature { get; } + + /// + /// Creates a registration record for with native storage support. /// /// The matching WebGPU texture format. /// The initialized registration. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CompositePixelRegistration Create(TextureFormat textureFormat) where TPixel : unmanaged, IPixel - => new(typeof(TPixel), textureFormat, Unsafe.SizeOf()); + => new(typeof(TPixel), textureFormat, Unsafe.SizeOf(), FeatureName.Undefined); + + /// + /// Creates a registration record for with a required device feature. + /// + /// The matching WebGPU texture format. + /// The device feature required for storage binding. + /// The initialized registration. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompositePixelRegistration Create(TextureFormat textureFormat, FeatureName requiredFeature) + where TPixel : unmanaged, IPixel + => new(typeof(TPixel), textureFormat, Unsafe.SizeOf(), requiredFeature); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index f6a3468a2..ac2c8b041 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -759,8 +759,43 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void } using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); - DeviceDescriptor descriptor = default; - api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + + // This path creates a device internally when the caller has not provided + // shared handles via WebGPURuntime.SetSharedHandles(). This happens when + // the WebGPU backend is used with a CPU-backed ICanvasFrame (e.g. Image) + // rather than a native surface — the backend still accelerates composition on + // the GPU but must provision its own device since no external context exists. + // + // Request optional storage features that are available on this adapter. + // The compute compositor needs storage binding on the transient output texture, + // and some formats (e.g. Bgra8Unorm) require explicit device features. + Span requestedFeatures = stackalloc FeatureName[1]; + int requestedCount = 0; + if (api.AdapterHasFeature(adapter, FeatureName.Bgra8UnormStorage)) + { + requestedFeatures[requestedCount++] = FeatureName.Bgra8UnormStorage; + } + + DeviceDescriptor descriptor; + if (requestedCount > 0) + { + fixed (FeatureName* featuresPtr = requestedFeatures) + { + descriptor = new DeviceDescriptor + { + RequiredFeatureCount = (uint)requestedCount, + RequiredFeatures = featuresPtr, + }; + + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + } + else + { + descriptor = default; + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + if (!WaitForSignal(callbackReady)) { device = null; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index 55253504e..0e6e06ceb 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -57,6 +57,11 @@ internal static unsafe class WebGPURuntime /// private static nint sharedQueueHandle; + /// + /// Set of device features available on the shared device. + /// + private static HashSet? deviceFeatures; + /// /// Number of currently active runtime leases. /// @@ -98,17 +103,50 @@ public static Lease Acquire() } } + /// + /// Sets shared GPU handles and device features for active backend execution. + /// + /// Opaque device handle. + /// Opaque queue handle. + /// Device features available on the shared device. + internal static void SetSharedHandles(nint deviceHandle, nint queueHandle, HashSet? features) + { + lock (Sync) + { + sharedDeviceHandle = deviceHandle; + sharedQueueHandle = queueHandle; + deviceFeatures = features; + } + } + /// /// Sets shared GPU handles for active backend execution. + /// Device features are queried automatically from the device. /// /// Opaque device handle. /// Opaque queue handle. internal static void SetSharedHandles(nint deviceHandle, nint queueHandle) { + // Ensure the API loader is initialized so we can enumerate device features. + using Lease lease = Acquire(); lock (Sync) { sharedDeviceHandle = deviceHandle; sharedQueueHandle = queueHandle; + deviceFeatures = EnumerateDeviceFeatures((Device*)deviceHandle); + } + } + + /// + /// Returns whether the shared device has the specified feature. + /// + /// The feature to check. + /// when the device has the feature; otherwise . + internal static bool HasDeviceFeature(FeatureName feature) + { + lock (Sync) + { + return deviceFeatures is not null && deviceFeatures.Contains(feature); } } @@ -121,6 +159,7 @@ internal static void ClearSharedHandles() { sharedDeviceHandle = 0; sharedQueueHandle = 0; + deviceFeatures = null; } } @@ -215,6 +254,7 @@ private static void DisposeRuntimeCore() { sharedDeviceHandle = 0; sharedQueueHandle = 0; + deviceFeatures = null; try { @@ -243,6 +283,36 @@ private static void DisposeRuntimeCore() } } + /// + /// Enumerates features on a device. + /// + /// The device to query. + /// A set of features supported by the device. + private static HashSet? EnumerateDeviceFeatures(Device* device) + { + if (api is null || device is null) + { + return null; + } + + int count = (int)api.DeviceEnumerateFeatures(device, (FeatureName*)null); + if (count <= 0) + { + return []; + } + + FeatureName* features = stackalloc FeatureName[count]; + api.DeviceEnumerateFeatures(device, features); + + HashSet result = new(count); + for (int i = 0; i < count; i++) + { + result.Add(features[i]); + } + + return result; + } + /// /// Ref-counted access token for . /// From aee48a58351672206a88d99f1b8d634547a980e1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 18:08:02 +1000 Subject: [PATCH 104/136] Make API public --- ImageSharp.Drawing.sln | 1 + samples/WebGPUWindowDemo/Program.cs | 50 +-- src/Directory.Build.props | 1 - .../ImageSharp.Drawing.WebGPU.csproj | 2 + .../WebGPUDrawingBackend.CompositePixels.cs | 36 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 5 +- .../WebGPUDrawingBackend.Readback.cs | 14 +- .../WebGPUDrawingBackend.cs | 50 ++- .../WebGPUFlushContext.cs | 308 ++++-------------- .../WebGPUNativeSurfaceFactory.cs | 19 +- .../WebGPURuntime.cs | 288 ++++++++++------ .../WebGPUSurfaceCapability.cs | 28 +- .../WebGPUTestNativeSurfaceAllocator.cs | 56 ++-- .../WebGPUTextureFormatId.cs | 73 +---- .../ImageSharp.Drawing.csproj | 5 - .../Processing/Backends/CompositionBatch.cs | 10 +- .../Processing/Backends/CompositionCommand.cs | 2 +- .../Backends/CompositionCoverageDefinition.cs | 8 +- .../Processing/Backends/CompositionScene.cs | 6 +- .../Backends/CompositionScenePlanner.cs | 2 +- .../Backends/DefaultDrawingBackend.cs | 2 +- .../Processing/Backends/IDrawingBackend.cs | 8 +- ...Pixel}.cs => MemoryCanvasFrame{TPixel}.cs} | 23 +- .../Backends/NativeCanvasFrame{TPixel}.cs | 46 +++ .../Backends/PreparedCompositionCommand.cs | 10 +- .../Processing/Backends/RasterizerOptions.cs | 6 +- .../Processing/DrawingCanvas{TPixel}.cs | 2 +- .../Processing/ImageBrush.cs | 15 +- .../RasterizerDefaultsExtensions.cs | 4 +- .../Drawing/DrawPolygon.cs | 65 +++- .../Drawing/DrawTextRepeatedGlyphs.cs | 61 +--- .../Backends/WebGPUDrawingBackendTests.cs | 25 +- .../WebGPUTextureFormatMapperTests.cs | 19 +- .../Processing/DrawingCanvasTests.Process.cs | 4 +- .../NativeSurfaceOnlyFrame{TPixel}.cs | 38 --- 35 files changed, 552 insertions(+), 740 deletions(-) rename src/ImageSharp.Drawing/Processing/Backends/{CpuCanvasFrame{TPixel}.cs => MemoryCanvasFrame{TPixel}.cs} (52%) create mode 100644 src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index e34ac25a0..c25885dcd 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -457,6 +457,7 @@ Global SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{061582c2-658f-40ae-a978-7d74a4eb2c0a}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index 1dcbbbcda..371640c83 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -12,7 +12,6 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Color = SixLabors.ImageSharp.Color; using Rectangle = SixLabors.ImageSharp.Rectangle; @@ -59,7 +58,7 @@ public static unsafe class Program // Scrolling text state — glyph geometry is built once at startup via TextBuilder // and translated vertically each frame. Only glyphs whose bounds intersect the // visible viewport are submitted for rasterization. - private static IPathCollection scrollPaths = null!; + private static IPathCollection scrollPaths; private static float scrollOffset; private static float scrollTextHeight; private const string ScrollText = @@ -156,10 +155,6 @@ private static void OnLoad() Console.WriteLine($"Device: 0x{(nuint)device:X}, Queue: 0x{(nuint)queue:X}"); - // Register shared handles so the drawing backend uses our device rather than - // provisioning its own. - WebGPURuntime.SetSharedHandles((nint)device, (nint)queue); - // Query surface capabilities and configure the swap chain. wgpu.SurfaceGetCapabilities(surface, adapter, ref surfaceCapabilities); Console.WriteLine($"Surface format: {surfaceCapabilities.Formats[0]}"); @@ -292,14 +287,11 @@ private static void OnRender(double deltaTime) (nint)textureView, WebGPUTextureFormatId.Bgra8Unorm, w, - h, - isSrgb: false, - isPremultipliedAlpha: false, - supportsTextureSampling: true); + h); - // NativeSurfaceOnlyFrame exposes only the GPU surface (no CPU region), + // NativeCanvasFrame exposes only the GPU surface (no CPU region), // so the backend always takes the GPU composition path. - NativeSurfaceOnlyFrame frame = new(new Rectangle(0, 0, w, h), nativeSurface); + NativeCanvasFrame frame = new(new Rectangle(0, 0, w, h), nativeSurface); // Create a drawing canvas targeting the swap chain frame. using DrawingCanvas canvas = new(drawingConfiguration, frame, new DrawingOptions()); @@ -389,15 +381,12 @@ private static void DrawScrollingText(DrawingCanvas canvas, int w, int h private static void OnClosing() { backend.Dispose(); - WebGPURuntime.ClearSharedHandles(); wgpu.DeviceRelease(device); wgpu.AdapterRelease(adapter); wgpu.SurfaceRelease(surface); wgpu.InstanceRelease(instance); wgpu.Dispose(); - - WebGPURuntime.Shutdown(); } /// WebGPU device-lost callback — logs the reason to the console. @@ -408,37 +397,6 @@ private static void DeviceLost(DeviceLostReason reason, byte* message, void* use private static void UncapturedError(ErrorType type, byte* message, void* userData) => Console.WriteLine($"WebGPU {type}: {SilkMarshal.PtrToString((nint)message)}"); - /// - /// Wraps a as an that - /// exposes only the GPU surface. returns false, ensuring - /// the drawing backend always takes the native GPU composition path. - /// - private sealed class NativeSurfaceOnlyFrame : ICanvasFrame - where TPixel : unmanaged, IPixel - { - private readonly NativeSurface nativeSurface; - - public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface nativeSurface) - { - this.Bounds = bounds; - this.nativeSurface = nativeSurface; - } - - public Rectangle Bounds { get; } - - public bool TryGetCpuRegion(out Buffer2DRegion region) - { - region = default; - return false; - } - - public bool TryGetNativeSurface(out NativeSurface surface) - { - surface = this.nativeSurface; - return true; - } - } - /// /// A simple bouncing ball with position, velocity, radius, and color. /// Reflects off the window edges each frame. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d833a7ac3..5ac8f2bf0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -30,7 +30,6 @@ - diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index fae29af27..8e6c3b941 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -67,6 +67,8 @@ + + diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index 1b2e59e6d..030548994 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// Only formats that support storage texture binding (required by the compute compositor) /// are included. Formats that lack storage support are omitted and fall back to the CPU backend. /// -internal sealed partial class WebGPUDrawingBackend +public sealed partial class WebGPUDrawingBackend { /// /// Builds the static registration table that maps implementations to @@ -27,9 +27,6 @@ internal sealed partial class WebGPUDrawingBackend private static Dictionary CreateCompositePixelHandlers() => // Only formats with native or feature-gated storage binding support. - // Non-storable formats (R8Unorm, RG8Unorm, RG8Snorm, R16Float, RG16Float, - // RG16Sint, Rgb10A2Unorm, R16Uint, RG16Uint) are omitted — they cannot be - // used as compute shader write targets and fall back to DefaultDrawingBackend. new() { [typeof(Byte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Uint), @@ -47,13 +44,12 @@ private static Dictionary CreateCompositePixel }; /// - /// Resolves the WebGPU texture format identifier for when supported - /// by the current device. + /// Resolves the WebGPU texture format identifier for . /// /// The requested pixel type. /// Receives the mapped texture format identifier on success. /// - /// when the pixel type is supported for GPU composition; otherwise . + /// when the pixel type has a registered GPU format mapping; otherwise . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) @@ -65,14 +61,36 @@ internal static bool TryGetCompositeTextureFormat(out WebGPUTextureForma return false; } - if (registration.RequiredFeature != FeatureName.Undefined - && !WebGPURuntime.HasDeviceFeature(registration.RequiredFeature)) + formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); + return true; + } + + /// + /// Resolves the WebGPU texture format identifier and any required device feature + /// for . + /// + /// The requested pixel type. + /// Receives the mapped texture format identifier on success. + /// + /// Receives the device feature required for storage binding, or + /// when no special feature is needed. + /// + /// + /// when the pixel type has a registered GPU format mapping; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration registration)) { formatId = default; + requiredFeature = FeatureName.Undefined; return false; } formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); + requiredFeature = registration.RequiredFeature; return true; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 2cf04478c..957724da3 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -10,7 +10,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -internal sealed unsafe partial class WebGPUDrawingBackend +/// +/// Coverage rasterization helpers. +/// +public sealed unsafe partial class WebGPUDrawingBackend { private const int TileHeight = 16; private const int EdgeStrideBytes = 16; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs index 564cba7a3..aa4044767 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -11,7 +11,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; -internal sealed unsafe partial class WebGPUDrawingBackend +/// +/// GPU readback helpers. +/// +public sealed unsafe partial class WebGPUDrawingBackend { private const int ReadbackCallbackTimeoutMilliseconds = 5000; @@ -46,7 +49,7 @@ public bool TryReadRegion( return false; } - if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId expectedFormat) || + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) || expectedFormat != capability.TargetFormat) { return false; @@ -68,6 +71,13 @@ public bool TryReadRegion( using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); WebGPU api = lease.Api; Device* device = (Device*)capability.Device; + + if (requiredFeature != FeatureName.Undefined + && !WebGPUFlushContext.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature)) + { + return false; + } + Queue* queue = (Queue*)capability.Queue; int pixelSizeInBytes = Unsafe.SizeOf(); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index c6a7099d6..3d8642fc8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -32,11 +32,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -> Blit once and optionally read back to CPU region /// -> On failure: delegate scene to DefaultDrawingBackend /// -/// -/// See src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md for a full process walkthrough. -/// /// -internal sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable +public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable { private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; @@ -119,29 +116,6 @@ public WebGPUDrawingBackend() /// internal int TestingComputePathBatchCount { get; private set; } - /// - /// Attempts to expose native WebGPU device and queue handles for interop. - /// - /// Receives the device pointer when available. - /// Receives the queue pointer when available. - /// when both handles are available; otherwise . - internal bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle) - { - this.ThrowIfDisposed(); - if (WebGPUFlushContext.TryGetInteropHandles(out deviceHandle, out queueHandle, out string? error)) - { - this.TestingGPUInitializationAttempted = true; - this.TestingIsGPUReady = true; - this.TestingLastGPUInitializationFailure = null; - return true; - } - - this.TestingGPUInitializationAttempted = true; - this.TestingIsGPUReady = false; - this.TestingLastGPUInitializationFailure = error; - return false; - } - /// public void FlushCompositions( Configuration configuration, @@ -158,7 +132,7 @@ public void FlushCompositions( return; } - if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) || + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature) || !AreAllCompositionBrushesSupported(compositionScene.Commands)) { int fallbackCommandCount = compositionScene.Commands.Count; @@ -232,13 +206,27 @@ public void FlushCompositions( string? failure = null; int pixelSizeInBytes = Unsafe.SizeOf(); - WebGPUFlushContext flushContext = WebGPUFlushContext.Create( + WebGPUFlushContext? flushContext = WebGPUFlushContext.Create( target, textureFormat, + requiredFeature, pixelSizeInBytes, configuration.MemoryAllocator, compositionBounds); + if (flushContext is null) + { + this.TestingFallbackPrepareCoverageCallCount += commandCount; + this.TestingFallbackCompositeCoverageCallCount += commandCount; + this.FlushCompositionsFallback( + configuration, + target, + compositionScene, + target.TryGetCpuRegion(out Buffer2DRegion _), + compositionBounds); + return; + } + try { gpuReady = true; @@ -253,7 +241,9 @@ public void FlushCompositions( compositionBounds.Value, commandCount, out failure); + bool finalizeOk = renderOk && this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); + gpuSuccess = finalizeOk; } catch (Exception ex) @@ -345,7 +335,7 @@ private void FlushCompositionsFallback( WebGPUFlushContext.RentFallbackStaging(configuration.MemoryAllocator, in targetBounds); Buffer2DRegion stagingRegion = stagingLease.Region; - ICanvasFrame stagingFrame = new CpuCanvasFrame(stagingRegion); + ICanvasFrame stagingFrame = new MemoryCanvasFrame(stagingRegion); this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene); using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target, configuration.MemoryAllocator); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index ac2c8b041..2c780df2a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -30,9 +30,6 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable { private static readonly ConcurrentDictionary FallbackStagingCache = new(); private static readonly ConcurrentDictionary DeviceStateCache = new(); - private static readonly object SharedHandleSync = new(); - private const int CallbackTimeoutMilliseconds = 10_000; - private bool disposed; private bool ownsTargetTexture; private bool ownsTargetView; @@ -129,11 +126,6 @@ private WebGPUFlushContext( /// public Texture* ReadbackSourceOverride { get; set; } - /// - /// Gets a value indicating whether the current target texture can be sampled in a compute shader. - /// - public bool CanSampleTargetTexture { get; private set; } - /// /// Gets the readback buffer used when CPU readback is required. /// @@ -177,9 +169,20 @@ private WebGPUFlushContext( /// /// Creates a flush context for either a native WebGPU surface or a CPU-backed frame. /// - public static WebGPUFlushContext Create( + /// The target frame. + /// The expected GPU texture format. + /// + /// A device feature required by the pixel type for storage binding, or + /// when no special feature is needed. + /// + /// The unmanaged pixel size in bytes. + /// The memory allocator for staging buffers. + /// Optional initial upload region for CPU targets. + /// The flush context, or if the device lacks . + public static WebGPUFlushContext? Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, + FeatureName requiredFeature, int pixelSizeInBytes, MemoryAllocator memoryAllocator, Rectangle? initialUploadBounds = null) @@ -203,6 +206,13 @@ public static WebGPUFlushContext Create( textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); bounds = new Rectangle(0, 0, nativeCapability.Width, nativeCapability.Height); deviceState = GetOrCreateDeviceState(lease.Api, device); + + if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) + { + lease.Dispose(); + return null; + } + context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, memoryAllocator, deviceState); context.InitializeNativeTarget(nativeCapability); return context; @@ -213,12 +223,19 @@ public static WebGPUFlushContext Create( throw new NotSupportedException("Frame does not expose a GPU-native surface or CPU region."); } - if (!TryGetOrCreateSharedHandles(lease.Api, out device, out queue, out string? error)) + if (!WebGPURuntime.TryGetOrCreateDevice(out device, out queue, out string? error)) { - throw new InvalidOperationException(error ?? "WebGPU shared handles are unavailable."); + throw new InvalidOperationException(error ?? "WebGPU device auto-provisioning failed."); } deviceState = GetOrCreateDeviceState(lease.Api, device); + + if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) + { + lease.Dispose(); + return null; + } + context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, memoryAllocator, deviceState); context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, initialUploadBounds); return context; @@ -304,36 +321,6 @@ public static void ClearDeviceStateCache() DeviceStateCache.Clear(); } - /// - /// Tries to get shared native interop handles for the active WebGPU device and queue. - /// - /// When this method returns , contains the native device handle. - /// When this method returns , contains the native queue handle. - /// When this method returns , contains an error message. - /// if shared handles are available; otherwise . - public static bool TryGetInteropHandles(out nint deviceHandle, out nint queueHandle, out string? error) - { - if (WebGPURuntime.TryGetSharedHandles(out Device* sharedDevice, out Queue* sharedQueue)) - { - deviceHandle = (nint)sharedDevice; - queueHandle = (nint)sharedQueue; - error = null; - return true; - } - - using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); - if (TryGetOrCreateSharedHandles(lease.Api, out Device* device, out Queue* queue, out error)) - { - deviceHandle = (nint)device; - queueHandle = (nint)queue; - return true; - } - - deviceHandle = 0; - queueHandle = 0; - return false; - } - /// /// Ensures that the instance buffer exists and can hold at least the requested number of bytes. /// @@ -592,7 +579,6 @@ public void Dispose() this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; this.RequiresReadback = false; - this.CanSampleTargetTexture = false; this.ownsReadbackBuffer = false; this.ownsTargetView = false; this.ownsTargetTexture = false; @@ -601,7 +587,7 @@ public void Dispose() this.disposed = true; } - private static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* device) + internal static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* device) { nint cacheKey = (nint)device; if (DeviceStateCache.TryGetValue(cacheKey, out DeviceSharedState? existing)) @@ -619,211 +605,11 @@ private static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* devi return winner; } - private static bool TryGetOrCreateSharedHandles( - WebGPU api, - out Device* device, - out Queue* queue, - out string? error) - { - if (WebGPURuntime.TryGetSharedHandles(out device, out queue)) - { - error = null; - return true; - } - - lock (SharedHandleSync) - { - if (WebGPURuntime.TryGetSharedHandles(out device, out queue)) - { - error = null; - return true; - } - - Instance* instance = api.CreateInstance((InstanceDescriptor*)null); - if (instance is null) - { - error = "WebGPU.CreateInstance returned null."; - device = null; - queue = null; - return false; - } - - Adapter* adapter = null; - Device* requestedDevice = null; - Queue* requestedQueue = null; - bool initialized = false; - try - { - if (!TryRequestAdapter(api, instance, out adapter, out error)) - { - device = null; - queue = null; - return false; - } - - if (!TryRequestDevice(api, adapter, out requestedDevice, out error)) - { - device = null; - queue = null; - return false; - } - - requestedQueue = api.DeviceGetQueue(requestedDevice); - if (requestedQueue is null) - { - error = "WebGPU.DeviceGetQueue returned null."; - device = null; - queue = null; - return false; - } - - WebGPURuntime.SetSharedHandles((nint)requestedDevice, (nint)requestedQueue); - device = requestedDevice; - queue = requestedQueue; - error = null; - initialized = true; - return true; - } - finally - { - if (adapter is not null) - { - api.AdapterRelease(adapter); - } - - api.InstanceRelease(instance); - - if (!initialized) - { - if (requestedQueue is not null) - { - api.QueueRelease(requestedQueue); - } - - if (requestedDevice is not null) - { - api.DeviceRelease(requestedDevice); - } - } - } - } - } - - private static bool TryRequestAdapter(WebGPU api, Instance* instance, out Adapter* adapter, out string? error) - { - RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; - Adapter* callbackAdapter = null; - using ManualResetEventSlim callbackReady = new(false); - void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) - { - callbackStatus = status; - callbackAdapter = adapterPtr; - callbackReady.Set(); - } - - using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); - RequestAdapterOptions options = new() - { - PowerPreference = PowerPreference.HighPerformance - }; - - api.InstanceRequestAdapter(instance, in options, callbackPtr, null); - if (!WaitForSignal(callbackReady)) - { - adapter = null; - error = "Timed out while waiting for WebGPU adapter request callback."; - return false; - } - - adapter = callbackAdapter; - if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) - { - error = $"WebGPU adapter request failed with status '{callbackStatus}'."; - return false; - } - - error = null; - return true; - } - - private static bool TryRequestDevice(WebGPU api, Adapter* adapter, out Device* device, out string? error) - { - RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; - Device* callbackDevice = null; - using ManualResetEventSlim callbackReady = new(false); - void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) - { - callbackStatus = status; - callbackDevice = devicePtr; - callbackReady.Set(); - } - - using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); - - // This path creates a device internally when the caller has not provided - // shared handles via WebGPURuntime.SetSharedHandles(). This happens when - // the WebGPU backend is used with a CPU-backed ICanvasFrame (e.g. Image) - // rather than a native surface — the backend still accelerates composition on - // the GPU but must provision its own device since no external context exists. - // - // Request optional storage features that are available on this adapter. - // The compute compositor needs storage binding on the transient output texture, - // and some formats (e.g. Bgra8Unorm) require explicit device features. - Span requestedFeatures = stackalloc FeatureName[1]; - int requestedCount = 0; - if (api.AdapterHasFeature(adapter, FeatureName.Bgra8UnormStorage)) - { - requestedFeatures[requestedCount++] = FeatureName.Bgra8UnormStorage; - } - - DeviceDescriptor descriptor; - if (requestedCount > 0) - { - fixed (FeatureName* featuresPtr = requestedFeatures) - { - descriptor = new DeviceDescriptor - { - RequiredFeatureCount = (uint)requestedCount, - RequiredFeatures = featuresPtr, - }; - - api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); - } - } - else - { - descriptor = default; - api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); - } - - if (!WaitForSignal(callbackReady)) - { - device = null; - error = "Timed out while waiting for WebGPU device request callback."; - return false; - } - - device = callbackDevice; - if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) - { - error = $"WebGPU device request failed with status '{callbackStatus}'."; - return false; - } - - error = null; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool WaitForSignal(ManualResetEventSlim signal) - => signal.Wait(CallbackTimeoutMilliseconds); - private void InitializeNativeTarget(WebGPUSurfaceCapability capability) { this.TargetTexture = (Texture*)capability.TargetTexture; this.TargetView = (TextureView*)capability.TargetTextureView; this.RequiresReadback = false; - this.CanSampleTargetTexture = capability.SupportsTextureSampling; this.ReadbackBuffer = null; this.ReadbackBytesPerRow = 0; this.ReadbackByteCount = 0; @@ -878,7 +664,6 @@ private void InitializeCpuTarget( this.ReadbackBytesPerRow = readbackRowBytes; this.ReadbackByteCount = readbackByteCount; this.RequiresReadback = true; - this.CanSampleTargetTexture = true; this.ownsTargetTexture = false; this.ownsTargetView = false; this.ownsReadbackBuffer = false; @@ -1039,12 +824,14 @@ internal sealed class DeviceSharedState : IDisposable private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary sharedBuffers = new(StringComparer.Ordinal); + private readonly HashSet deviceFeatures; private bool disposed; internal DeviceSharedState(WebGPU api, Device* device) { this.Api = api; this.Device = device; + this.deviceFeatures = EnumerateDeviceFeatures(api, device); } private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; @@ -1068,6 +855,39 @@ internal DeviceSharedState(WebGPU api, Device* device) /// public Device* Device { get; } + /// + /// Returns whether the device has the specified feature. + /// + /// The feature to check. + /// when the device has the feature; otherwise . + public bool HasFeature(FeatureName feature) + => this.deviceFeatures.Contains(feature); + + private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* device) + { + if (device is null) + { + return []; + } + + int count = (int)api.DeviceEnumerateFeatures(device, (FeatureName*)null); + if (count <= 0) + { + return []; + } + + FeatureName* features = stackalloc FeatureName[count]; + api.DeviceEnumerateFeatures(device, features); + + HashSet result = new(count); + for (int i = 0; i < count; i++) + { + result.Add(features[i]); + } + + return result; + } + /// /// Rents CPU-target staging resources for a destination texture shape and format. /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs index 2b9878c0f..014ef8f2c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs @@ -21,12 +21,11 @@ public static class WebGPUNativeSurfaceFactory /// Texture format identifier. /// Surface width in pixels. /// Surface height in pixels. - /// Whether the surface is sRGB encoded. - /// Whether surface alpha is premultiplied. - /// - /// Whether supports texture sampling. - /// /// A configured instance. + /// + /// The target texture must have been created with the TEXTURE_BINDING usage flag. + /// The backend reads the target texture for Porter-Duff backdrop sampling. + /// public static NativeSurface Create( nint deviceHandle, nint queueHandle, @@ -34,10 +33,7 @@ public static NativeSurface Create( nint targetTextureViewHandle, WebGPUTextureFormatId targetFormat, int width, - int height, - bool isSrgb, - bool isPremultipliedAlpha, - bool supportsTextureSampling) + int height) where TPixel : unmanaged, IPixel { ValidateCommon( @@ -57,10 +53,7 @@ public static NativeSurface Create( targetTextureViewHandle, targetFormat, width, - height, - isSrgb, - isPremultipliedAlpha, - supportsTextureSampling)); + height)); return nativeSurface; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index 0e6e06ceb..268f22ae9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -11,8 +11,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// -/// This type owns the process-level Silk API loader and its -/// optional extension. +/// This type owns the process-level Silk API loader, its +/// optional extension, and a lazily provisioned default +/// device/queue pair used by the GPU backend when no native surface is available. /// /// /// Backends acquire access by taking a via . @@ -48,19 +49,14 @@ internal static unsafe class WebGPURuntime private static Wgpu? wgpuExtension; /// - /// Shared device handle used by active backends in the current process. + /// Lazily provisioned device handle for CPU-backed frames. /// - private static nint sharedDeviceHandle; + private static nint autoDeviceHandle; /// - /// Shared queue handle used by active backends in the current process. + /// Lazily provisioned queue handle for CPU-backed frames. /// - private static nint sharedQueueHandle; - - /// - /// Set of device features available on the shared device. - /// - private static HashSet? deviceFeatures; + private static nint autoQueueHandle; /// /// Number of currently active runtime leases. @@ -72,6 +68,11 @@ internal static unsafe class WebGPURuntime /// private static bool processExitHooked; + /// + /// Timeout for asynchronous WebGPU callbacks. + /// + private const int CallbackTimeoutMilliseconds = 10_000; + /// /// Acquires a runtime lease for WebGPU access. /// @@ -104,85 +105,107 @@ public static Lease Acquire() } /// - /// Sets shared GPU handles and device features for active backend execution. - /// - /// Opaque device handle. - /// Opaque queue handle. - /// Device features available on the shared device. - internal static void SetSharedHandles(nint deviceHandle, nint queueHandle, HashSet? features) - { - lock (Sync) - { - sharedDeviceHandle = deviceHandle; - sharedQueueHandle = queueHandle; - deviceFeatures = features; - } - } - - /// - /// Sets shared GPU handles for active backend execution. - /// Device features are queried automatically from the device. + /// Lazily provisions and caches a default device/queue pair for CPU-backed frames. + /// Returns cached handles on subsequent calls. /// - /// Opaque device handle. - /// Opaque queue handle. - internal static void SetSharedHandles(nint deviceHandle, nint queueHandle) - { - // Ensure the API loader is initialized so we can enumerate device features. - using Lease lease = Acquire(); - lock (Sync) - { - sharedDeviceHandle = deviceHandle; - sharedQueueHandle = queueHandle; - deviceFeatures = EnumerateDeviceFeatures((Device*)deviceHandle); - } - } - - /// - /// Returns whether the shared device has the specified feature. - /// - /// The feature to check. - /// when the device has the feature; otherwise . - internal static bool HasDeviceFeature(FeatureName feature) + /// Receives the device pointer on success. + /// Receives the queue pointer on success. + /// Receives an error message on failure. + /// when handles are available; otherwise . + internal static bool TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? error) { lock (Sync) { - return deviceFeatures is not null && deviceFeatures.Contains(feature); - } - } + // Fast path: return cached handles. + if (autoDeviceHandle != 0 && autoQueueHandle != 0) + { + device = (Device*)autoDeviceHandle; + queue = (Queue*)autoQueueHandle; + error = null; + return true; + } - /// - /// Clears shared GPU handles. - /// - internal static void ClearSharedHandles() - { - lock (Sync) - { - sharedDeviceHandle = 0; - sharedQueueHandle = 0; - deviceFeatures = null; - } - } + if (api is null) + { + device = null; + queue = null; + error = "WebGPU API is not initialized. Call Acquire() first."; + return false; + } - /// - /// Attempts to get shared GPU handles. - /// - /// Receives the shared device pointer. - /// Receives the shared queue pointer. - /// when handles are available; otherwise . - internal static bool TryGetSharedHandles(out Device* device, out Queue* queue) - { - lock (Sync) - { - if (sharedDeviceHandle == 0 || sharedQueueHandle == 0) + // Provision: instance → adapter → device → queue. + // The instance and adapter are transient; only the device and queue are cached. + Instance* instance = api.CreateInstance((InstanceDescriptor*)null); + if (instance is null) { device = null; queue = null; + error = "WebGPU.CreateInstance returned null."; return false; } - device = (Device*)sharedDeviceHandle; - queue = (Queue*)sharedQueueHandle; - return true; + Adapter* adapter = null; + Device* requestedDevice = null; + Queue* requestedQueue = null; + bool initialized = false; + try + { + if (!TryRequestAdapter(api, instance, out adapter, out error)) + { + device = null; + queue = null; + return false; + } + + if (!TryRequestDevice(api, adapter, out requestedDevice, out error)) + { + device = null; + queue = null; + return false; + } + + requestedQueue = api.DeviceGetQueue(requestedDevice); + if (requestedQueue is null) + { + device = null; + queue = null; + error = "WebGPU.DeviceGetQueue returned null."; + return false; + } + + // Cache for subsequent calls. + autoDeviceHandle = (nint)requestedDevice; + autoQueueHandle = (nint)requestedQueue; + device = requestedDevice; + queue = requestedQueue; + error = null; + initialized = true; + return true; + } + finally + { + // Always release transient handles. + if (adapter is not null) + { + api.AdapterRelease(adapter); + } + + api.InstanceRelease(instance); + + // On failure, release any partially provisioned handles. + if (!initialized) + { + if (requestedQueue is not null) + { + api.QueueRelease(requestedQueue); + } + + if (requestedDevice is not null) + { + api.DeviceRelease(requestedDevice); + } + } + } } } @@ -252,9 +275,8 @@ private static void OnProcessExit(object? sender, EventArgs e) /// private static void DisposeRuntimeCore() { - sharedDeviceHandle = 0; - sharedQueueHandle = 0; - deviceFeatures = null; + autoDeviceHandle = 0; + autoQueueHandle = 0; try { @@ -283,34 +305,104 @@ private static void DisposeRuntimeCore() } } - /// - /// Enumerates features on a device. - /// - /// The device to query. - /// A set of features supported by the device. - private static HashSet? EnumerateDeviceFeatures(Device* device) + private static bool TryRequestAdapter(WebGPU api, Instance* instance, out Adapter* adapter, out string? error) + { + RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; + Adapter* callbackAdapter = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) + { + callbackStatus = status; + callbackAdapter = adapterPtr; + callbackReady.Set(); + } + + using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); + RequestAdapterOptions options = new() + { + PowerPreference = PowerPreference.HighPerformance + }; + + api.InstanceRequestAdapter(instance, in options, callbackPtr, null); + if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) + { + adapter = null; + error = "Timed out while waiting for WebGPU adapter request callback."; + return false; + } + + adapter = callbackAdapter; + if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) + { + error = $"WebGPU adapter request failed with status '{callbackStatus}'."; + return false; + } + + error = null; + return true; + } + + private static bool TryRequestDevice(WebGPU api, Adapter* adapter, out Device* device, out string? error) { - if (api is null || device is null) + RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; + Device* callbackDevice = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) { - return null; + callbackStatus = status; + callbackDevice = devicePtr; + callbackReady.Set(); } - int count = (int)api.DeviceEnumerateFeatures(device, (FeatureName*)null); - if (count <= 0) + using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); + + // Auto-provision a device when no native surface provides one. + // Request optional storage features that are available on this adapter. + // The compute compositor needs storage binding on the transient output texture, + // and some formats (e.g. Bgra8Unorm) require explicit device features. + Span requestedFeatures = stackalloc FeatureName[1]; + int requestedCount = 0; + if (api.AdapterHasFeature(adapter, FeatureName.Bgra8UnormStorage)) { - return []; + requestedFeatures[requestedCount++] = FeatureName.Bgra8UnormStorage; } - FeatureName* features = stackalloc FeatureName[count]; - api.DeviceEnumerateFeatures(device, features); + DeviceDescriptor descriptor; + if (requestedCount > 0) + { + fixed (FeatureName* featuresPtr = requestedFeatures) + { + descriptor = new DeviceDescriptor + { + RequiredFeatureCount = (uint)requestedCount, + RequiredFeatures = featuresPtr, + }; + + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + } + else + { + descriptor = default; + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + + if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) + { + device = null; + error = "Timed out while waiting for WebGPU device request callback."; + return false; + } - HashSet result = new(count); - for (int i = 0; i < count; i++) + device = callbackDevice; + if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) { - result.Add(features[i]); + error = $"WebGPU device request failed with status '{callbackStatus}'."; + return false; } - return result; + error = null; + return true; } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs index edd082e2a..6a62cc51a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -25,11 +25,6 @@ public sealed class WebGPUSurfaceCapability /// Native render target texture format identifier. /// Surface width in pixels. /// Surface height in pixels. - /// Whether the target format is sRGB encoded. - /// Whether alpha is premultiplied in the target surface. - /// - /// Whether can be sampled as a texture binding. - /// public WebGPUSurfaceCapability( nint device, nint queue, @@ -37,10 +32,7 @@ public WebGPUSurfaceCapability( nint targetTextureView, WebGPUTextureFormatId targetFormat, int width, - int height, - bool isSrgb, - bool isPremultipliedAlpha, - bool supportsTextureSampling) + int height) { this.Device = device; this.Queue = queue; @@ -49,9 +41,6 @@ public WebGPUSurfaceCapability( this.TargetFormat = targetFormat; this.Width = width; this.Height = height; - this.IsSrgb = isSrgb; - this.IsPremultipliedAlpha = isPremultipliedAlpha; - this.SupportsTextureSampling = supportsTextureSampling; } /// @@ -88,19 +77,4 @@ public WebGPUSurfaceCapability( /// Gets the surface height in pixels. /// public int Height { get; } - - /// - /// Gets a value indicating whether the target format is sRGB encoded. - /// - public bool IsSrgb { get; } - - /// - /// Gets a value indicating whether the target uses premultiplied alpha. - /// - public bool IsPremultipliedAlpha { get; } - - /// - /// Gets a value indicating whether the target texture supports texture sampling. - /// - public bool SupportsTextureSampling { get; } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs index 69d467770..ed78b044c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -21,42 +21,46 @@ internal static unsafe class WebGPUTestNativeSurfaceAllocator /// Tries to allocate a native WebGPU texture + view pair and wrap them in a . /// internal static bool TryCreate( - WebGPUDrawingBackend backend, int width, int height, - bool isSrgb, - bool isPremultipliedAlpha, out NativeSurface surface, out nint textureHandle, out nint textureViewHandle, out string error) where TPixel : unmanaged, IPixel { - if (!backend.TryGetInteropHandles(out nint deviceHandle, out nint queueHandle)) + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)) { surface = new NativeSurface(TPixel.GetPixelTypeInfo()); textureHandle = 0; textureViewHandle = 0; - error = "WebGPU backend is not initialized."; + error = $"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."; return false; } - if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId)) + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? deviceError)) { surface = new NativeSurface(TPixel.GetPixelTypeInfo()); textureHandle = 0; textureViewHandle = 0; - error = $"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."; + error = deviceError ?? "WebGPU device auto-provisioning failed."; return false; } - TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); - - // Lease.Dispose only decrements the runtime ref-count; it does not dispose the shared WebGPU API. - using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); - WebGPU api = lease.Api; - Device* device = (Device*)deviceHandle; + if (requiredFeature != FeatureName.Undefined + && !WebGPUFlushContext.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = $"Device does not support required feature '{requiredFeature}' for pixel type '{typeof(TPixel).Name}'."; + return false; + } + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); TextureDescriptor targetTextureDescriptor = new() { Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding, @@ -99,6 +103,8 @@ internal static bool TryCreate( return false; } + nint deviceHandle = (nint)device; + nint queueHandle = (nint)queue; textureHandle = (nint)targetTexture; textureViewHandle = (nint)targetView; surface = WebGPUNativeSurfaceFactory.Create( @@ -108,10 +114,7 @@ internal static bool TryCreate( textureViewHandle, formatId, width, - height, - isSrgb, - isPremultipliedAlpha, - supportsTextureSampling: true); + height); error = string.Empty; return true; } @@ -120,7 +123,6 @@ internal static bool TryCreate( /// Tries to upload CPU pixel data to an existing native WebGPU texture handle. /// internal static bool TryWriteTexture( - WebGPUDrawingBackend backend, nint textureHandle, int width, int height, @@ -140,19 +142,19 @@ internal static bool TryWriteTexture( return false; } - if (!backend.TryGetInteropHandles(out _, out nint queueHandle)) + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (!WebGPURuntime.TryGetOrCreateDevice(out _, out Queue* queue, out string? deviceError)) { - error = backend.TestingLastGPUInitializationFailure ?? "WebGPU backend is not initialized."; + error = deviceError ?? "WebGPU device auto-provisioning failed."; return false; } try { - using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); Buffer2DRegion sourceRegion = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); WebGPUFlushContext.UploadTextureFromRegion( lease.Api, - (Queue*)queueHandle, + queue, (Texture*)textureHandle, sourceRegion, Configuration.Default.MemoryAllocator); @@ -170,7 +172,6 @@ internal static bool TryWriteTexture( /// Tries to read pixels from a native WebGPU texture handle into an . /// internal static bool TryReadTexture( - WebGPUDrawingBackend backend, nint textureHandle, int width, int height, @@ -191,17 +192,14 @@ internal static bool TryReadTexture( return false; } - if (!backend.TryGetInteropHandles(out nint deviceHandle, out nint queueHandle)) + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? deviceError)) { - error = backend.TestingLastGPUInitializationFailure ?? "WebGPU backend is not initialized."; + error = deviceError ?? "WebGPU device auto-provisioning failed."; return false; } - using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); WebGPU api = lease.Api; - Device* device = (Device*)deviceHandle; - Queue* queue = (Queue*)queueHandle; - int pixelSizeInBytes = Unsafe.SizeOf(); int packedRowBytes = checked(width * pixelSizeInBytes); int readbackRowBytes = Align(packedRowBytes, 256); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs index 33882361f..06cd5e06a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs @@ -4,26 +4,18 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Public WebGPU texture format identifiers used by . +/// Supported WebGPU texture format identifiers used by . /// +/// +/// Only formats with storage texture binding support are included. +/// Numeric values match the WebGPU WGPUTextureFormat constants. +/// public enum WebGPUTextureFormatId { - // Numeric values intentionally match WGPUTextureFormat. - - /// - /// Single-channel 8-bit normalized unsigned format. - /// - R8Unorm = 0x01, - - /// - /// Two-channel 8-bit normalized unsigned format. - /// - RG8Unorm = 0x08, - /// - /// Two-channel 8-bit normalized signed format. + /// Four-channel 8-bit normalized unsigned RGBA format. /// - RG8Snorm = 0x09, + Rgba8Unorm = 0x12, /// /// Four-channel 8-bit normalized signed format. @@ -31,24 +23,19 @@ public enum WebGPUTextureFormatId Rgba8Snorm = 0x14, /// - /// Single-channel 16-bit floating-point format. - /// - R16Float = 0x07, - - /// - /// Two-channel 16-bit floating-point format. + /// Four-channel 8-bit unsigned integer format. /// - RG16Float = 0x11, + Rgba8Uint = 0x15, /// - /// Four-channel 16-bit floating-point format. + /// Four-channel 8-bit normalized unsigned BGRA format. /// - Rgba16Float = 0x22, + Bgra8Unorm = 0x17, /// - /// Two-channel 16-bit signed integer format. + /// Four-channel 16-bit unsigned integer format. /// - RG16Sint = 0x10, + Rgba16Uint = 0x20, /// /// Four-channel 16-bit signed integer format. @@ -56,42 +43,12 @@ public enum WebGPUTextureFormatId Rgba16Sint = 0x21, /// - /// Packed 10:10:10:2 normalized unsigned format. - /// - Rgb10A2Unorm = 0x1A, - - /// - /// Four-channel 8-bit normalized unsigned RGBA format. - /// - Rgba8Unorm = 0x12, - - /// - /// Four-channel 8-bit normalized unsigned BGRA format. + /// Four-channel 16-bit floating-point format. /// - Bgra8Unorm = 0x17, + Rgba16Float = 0x22, /// /// Four-channel 32-bit floating-point format. /// Rgba32Float = 0x23, - - /// - /// Single-channel 16-bit unsigned integer format. - /// - R16Uint = 0x05, - - /// - /// Two-channel 16-bit unsigned integer format. - /// - RG16Uint = 0x0F, - - /// - /// Four-channel 16-bit unsigned integer format. - /// - Rgba16Uint = 0x20, - - /// - /// Four-channel 8-bit unsigned integer format. - /// - Rgba8Uint = 0x15 } diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 4d1374364..a57ea8bc8 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -40,11 +40,6 @@ - - - - - diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs index d05a451b2..9481872d2 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs @@ -6,8 +6,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Prepared composition data emitted by and consumed by backends. /// -internal sealed class CompositionBatch +public sealed class CompositionBatch { + /// + /// Initializes a new instance of the class. + /// + /// The coverage definition for this batch. + /// Prepared composition commands in draw order. + /// The flush identifier shared by all batches in one flush call. + /// Whether this is the last batch for the current flush. + /// Optional destination-local bounds touched by this batch. public CompositionBatch( in CompositionCoverageDefinition definition, IReadOnlyList commands, diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index c6f6ce237..4db3a8cd2 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One normalized composition command queued by . /// -internal readonly struct CompositionCommand +public readonly struct CompositionCommand { /// /// Initializes a new instance of the struct. diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs index f3973ccae..53d3f8d2f 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -6,8 +6,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One coverage definition that can be rasterized once and reused by multiple composition commands. /// -internal readonly struct CompositionCoverageDefinition +public readonly struct CompositionCoverageDefinition { + /// + /// Initializes a new instance of the struct. + /// + /// The stable key for this coverage definition. + /// The path used to generate coverage. + /// The rasterizer options used to generate coverage. public CompositionCoverageDefinition(int definitionKey, IPath path, in RasterizerOptions rasterizerOptions) { this.DefinitionKey = definitionKey; diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs index 0375d8ab5..12d5bfd31 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs @@ -6,8 +6,12 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One flush-time scene packet containing normalized composition commands in draw order. /// -internal sealed class CompositionScene +public sealed class CompositionScene { + /// + /// Initializes a new instance of the class. + /// + /// The composition commands in submission order. public CompositionScene(IReadOnlyList commands) => this.Commands = commands; diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs index 6cfe07d8b..36e9cee3c 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Converts scene command streams into backend-ready prepared batches. /// -internal static class CompositionScenePlanner +public static class CompositionScenePlanner { /// /// Creates contiguous prepared batches grouped by coverage definition key. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 3c536e116..474b86047 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// -internal sealed class DefaultDrawingBackend : IDrawingBackend +public sealed class DefaultDrawingBackend : IDrawingBackend { /// /// Gets the default backend instance. diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 13cbf52ae..29e5e5b58 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -6,13 +6,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Internal drawing backend abstraction used by processors. +/// Drawing backend abstraction used by processors. /// -/// -/// This boundary allows processor logic to stay stable while the implementation evolves -/// (for example: alternate CPU rasterizers or eventual non-CPU backends). -/// -internal interface IDrawingBackend +public interface IDrawingBackend { /// /// Flushes queued composition operations for the target. diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs similarity index 52% rename from src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs rename to src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs index 29eefb190..6105eca8e 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CpuCanvasFrame{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs @@ -7,33 +7,38 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Canvas frame adapter over a CPU . +/// Canvas frame adapter over a . /// /// The pixel format. -internal sealed class CpuCanvasFrame : ICanvasFrame +public sealed class MemoryCanvasFrame : ICanvasFrame where TPixel : unmanaged, IPixel { private readonly Buffer2DRegion region; - private readonly NativeSurface? nativeSurface; - public CpuCanvasFrame(Buffer2DRegion region, NativeSurface? nativeSurface = null) + /// + /// Initializes a new instance of the class. + /// + /// The pixel buffer region backing this frame. + public MemoryCanvasFrame(Buffer2DRegion region) { Guard.NotNull(region.Buffer, nameof(region)); this.region = region; - this.nativeSurface = nativeSurface; } + /// public Rectangle Bounds => this.region.Rectangle; - public bool TryGetCpuRegion(out Buffer2DRegion cpuRegion) + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) { - cpuRegion = this.region; + region = this.region; return true; } + /// public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) { - surface = this.nativeSurface; - return surface is not null; + surface = null; + return false; } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs new file mode 100644 index 000000000..b09d39b5a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Canvas frame adapter over a . +/// +/// The pixel format. +public sealed class NativeCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly NativeSurface surface; + + /// + /// Initializes a new instance of the class. + /// + /// The frame bounds. + /// The native surface backing this frame. + public NativeCanvasFrame(Rectangle bounds, NativeSurface surface) + { + Guard.NotNull(surface, nameof(surface)); + this.Bounds = bounds; + this.surface = surface; + } + + /// + public Rectangle Bounds { get; } + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = this.surface; + return true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs index 44ad6b175..4fe2864e6 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs @@ -6,8 +6,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One normalized composition command that applies a brush to the active coverage map. /// -internal readonly struct PreparedCompositionCommand +public readonly struct PreparedCompositionCommand { + /// + /// Initializes a new instance of the struct. + /// + /// The destination region in target-local coordinates. + /// The source offset into the pre-rasterized coverage map. + /// The brush used during composition. + /// Brush bounds used for applicator creation. + /// Graphics options used during composition. public PreparedCompositionCommand( Rectangle destinationRegion, Point sourceOffset, diff --git a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 01d1d93b0..9f1639047 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Describes whether rasterizers should emit continuous coverage or binary aliased coverage. /// -internal enum RasterizationMode +public enum RasterizationMode { /// /// Emit continuous coverage in the range [0, 1]. @@ -22,7 +22,7 @@ internal enum RasterizationMode /// /// Describes where sample coverage is aligned relative to destination pixels. /// -internal enum RasterizerSamplingOrigin +public enum RasterizerSamplingOrigin { /// /// Samples are aligned to pixel boundaries. @@ -38,7 +38,7 @@ internal enum RasterizerSamplingOrigin /// /// Immutable options used by rasterizers when scan-converting vector geometry. /// -internal readonly struct RasterizerOptions +public readonly struct RasterizerOptions { /// /// Initializes a new instance of the struct. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 02b992e4d..e908c1f4f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -69,7 +69,7 @@ public DrawingCanvas( Buffer2DRegion targetRegion, DrawingOptions options, params IPath[] clipPaths) - : this(configuration, new CpuCanvasFrame(targetRegion), options, clipPaths) + : this(configuration, new MemoryCanvasFrame(targetRegion), options, clipPaths) { } diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 6198c2aa4..e0a154a50 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -63,11 +63,20 @@ public ImageBrush(Image image, RectangleF region, Point offset) this.Offset = offset; } - internal Image SourceImage { get; } + /// + /// Gets the source image used by this brush. + /// + public Image SourceImage { get; } - internal RectangleF SourceRegion { get; } + /// + /// Gets the source region within the image. + /// + public RectangleF SourceRegion { get; } - internal Point Offset { get; } + /// + /// Gets the offset applied to the brush origin. + /// + public Point Offset { get; } /// public override bool Equals(Brush? other) diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs index 343dc9069..f662091d1 100644 --- a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Adds extensions that allow configuring the drawing backend implementation. /// -internal static class RasterizerDefaultsExtensions +public static class RasterizerDefaultsExtensions { /// /// Sets the drawing backend against the source image processing context. @@ -29,7 +29,7 @@ internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingC /// /// The configuration to store the backend against. /// The backend to use. - internal static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend) + public static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend) { Guard.NotNull(backend, nameof(backend)); configuration.Properties[typeof(IDrawingBackend)] = backend; diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 6c6b1dd98..54d54d414 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -41,6 +41,9 @@ public abstract class DrawPolygon private IPath strokedImageSharpPath; private WebGPUDrawingBackend webGpuBackend; private Configuration webGpuConfiguration; + private NativeCanvasFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; protected abstract int Width { get; } @@ -49,7 +52,7 @@ public abstract class DrawPolygon protected abstract float Thickness { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) => - features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray(); + [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60)))]; [GlobalSetup] public void Setup() @@ -115,6 +118,22 @@ public void Setup() this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); this.webGpuImage = new Image(this.webGpuConfiguration, this.Width, this.Height); + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + this.Width, + this.Height, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); + } + + this.webGpuNativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, this.Width, this.Height), + nativeSurface); + this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); this.sdGraphics.InterpolationMode = InterpolationMode.Default; @@ -156,38 +175,52 @@ public void Cleanup() this.image.Dispose(); this.webGpuImage.Dispose(); + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; this.webGpuBackend.Dispose(); } + [Benchmark(Baseline = true)] + public void SkiaSharp() + => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); + [Benchmark] public void SystemDrawing() => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); [Benchmark] - public void ImageSharpCombinedPaths() + public void ImageSharp() => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] public void ImageSharpCombinedPathsWebGPUBackend() => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); - [Benchmark] - public void ImageSharpSeparatePaths() - => this.image.Mutate( - c => c.ProcessWithCanvas(canvas => - { - foreach (PointF[] loop in this.points) - { - canvas.Draw(Processing.Pens.Solid(Color.White, this.Thickness), new Polygon(loop)); - } - })); + [Benchmark(Description = "ImageSharp Combined Paths WebGPU NativeSurface")] + public void ImageSharpCombinedPathsWebGPUNativeSurface() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, new DrawingOptions()); + canvas.Draw(this.isPen, this.imageSharpPath); + canvas.Flush(); + } - [Benchmark(Baseline = true)] - public void SkiaSharp() - => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); + [Benchmark] + public IPath ImageSharpStrokeAndClipCombined() => this.isPen.GeneratePath(this.imageSharpPath); [Benchmark] - public IPath ImageSharpStrokeAndClip() => this.isPen.GeneratePath(this.imageSharpPath); + public IPath ImageSharpStrokeAndClipSeparate() + { + IPath path = Path.Empty; + foreach (PointF[] loop in this.points) + { + path = this.isPen.GeneratePath(new Polygon(loop)); + } + + return path; + } [Benchmark] public void FillPolygon() diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 122c7cd50..68961bf21 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -33,7 +33,7 @@ public class DrawTextRepeatedGlyphs private Image webGpuCpuImage; private WebGPUDrawingBackend webGpuBackend; private Configuration webGpuConfiguration; - private NativeSurfaceOnlyFrame webGpuNativeFrame; + private NativeCanvasFrame webGpuNativeFrame; private nint webGpuNativeTextureHandle; private nint webGpuNativeTextureViewHandle; private RichTextOptions textOptions; @@ -67,21 +67,18 @@ public void Setup() this.webGpuCpuImage = new Image(this.webGpuConfiguration, Width, Height); if (!WebGPUTestNativeSurfaceAllocator.TryCreate( - this.webGpuBackend, Width, Height, - isSrgb: false, - isPremultipliedAlpha: false, out NativeSurface nativeSurface, out this.webGpuNativeTextureHandle, out this.webGpuNativeTextureViewHandle, out string nativeSurfaceError)) { throw new InvalidOperationException( - $"Unable to create benchmark native WebGPU target. GPUReady={this.webGpuBackend.TestingIsGPUReady}, Error='{(nativeSurfaceError.Length > 0 ? nativeSurfaceError : this.webGpuBackend.TestingLastGPUInitializationFailure ?? "")}'."); + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); } - this.webGpuNativeFrame = new NativeSurfaceOnlyFrame( + this.webGpuNativeFrame = new NativeCanvasFrame( new Rectangle(0, 0, Width, Height), nativeSurface); @@ -104,7 +101,7 @@ public void Cleanup() [Benchmark(Baseline = true, Description = "DrawingCanvas Default Backend")] public void DrawingCanvasDefaultBackend() { - CpuRegionOnlyFrame frame = new(GetFrameRegion(this.defaultImage)); + MemoryCanvasFrame frame = new(GetFrameRegion(this.defaultImage)); using DrawingCanvas canvas = new(this.defaultConfiguration, frame, this.drawingOptions); canvas.DrawText(this.textOptions, this.text, this.brush, null); @@ -114,7 +111,7 @@ public void DrawingCanvasDefaultBackend() [Benchmark(Description = "DrawingCanvas WebGPU Backend (CPURegion)")] public void DrawingCanvasWebGPUBackendCpuRegion() { - CpuRegionOnlyFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); + MemoryCanvasFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); using DrawingCanvas canvas = new(this.webGpuConfiguration, frame, this.drawingOptions); canvas.DrawText(this.textOptions, this.text, this.brush, null); @@ -131,52 +128,4 @@ public void DrawingCanvasWebGPUBackendNativeSurface() private static Buffer2DRegion GetFrameRegion(Image image) => new(image.Frames.RootFrame.PixelBuffer, new Rectangle(0, 0, image.Width, image.Height)); - - private sealed class CpuRegionOnlyFrame : ICanvasFrame - where TPixel : unmanaged, IPixel - { - private readonly Buffer2DRegion region; - - public CpuRegionOnlyFrame(Buffer2DRegion region) => this.region = region; - - public Rectangle Bounds => this.region.Rectangle; - - public bool TryGetCpuRegion(out Buffer2DRegion region) - { - region = this.region; - return true; - } - - public bool TryGetNativeSurface(out NativeSurface surface) - { - surface = default; - return false; - } - } - - private sealed class NativeSurfaceOnlyFrame : ICanvasFrame - where TPixel : unmanaged, IPixel - { - private readonly NativeSurface surface; - - public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) - { - this.Bounds = bounds; - this.surface = surface; - } - - public Rectangle Bounds { get; } - - public bool TryGetCpuRegion(out Buffer2DRegion region) - { - region = default; - return false; - } - - public bool TryGetNativeSurface(out NativeSurface surface) - { - surface = this.surface; - return true; - } - } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 6637f0895..39bf68f94 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -9,7 +9,6 @@ using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -695,11 +694,8 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes using WebGPUDrawingBackend nativeSurfaceBackend = new(); Assert.True( WebGPUTestNativeSurfaceAllocator.TryCreate( - nativeSurfaceBackend, defaultImage.Width, defaultImage.Height, - isSrgb: false, - isPremultipliedAlpha: false, out NativeSurface nativeSurface, out nint textureHandle, out nint textureViewHandle, @@ -713,7 +709,7 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes Rectangle targetBounds = defaultImage.Bounds; using (DrawingCanvas nativeSurfaceClearCanvas = - new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), clearOptions)) + new(nativeSurfaceConfiguration, new NativeCanvasFrame(targetBounds, nativeSurface), clearOptions)) { nativeSurfaceClearCanvas.Fill(clearBrush); nativeSurfaceClearCanvas.Flush(); @@ -721,7 +717,7 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes int nativeSurfaceComputeBatchesBeforeDraw = nativeSurfaceBackend.TestingComputePathBatchCount; using (DrawingCanvas nativeSurfaceDrawCanvas = - new(nativeSurfaceConfiguration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) + new(nativeSurfaceConfiguration, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) { nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawBrush, null); nativeSurfaceDrawCanvas.Flush(); @@ -732,7 +728,6 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes Assert.True( WebGPUTestNativeSurfaceAllocator.TryReadTexture( - nativeSurfaceBackend, textureHandle, defaultImage.Width, defaultImage.Height, @@ -843,11 +838,8 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( { Assert.True( WebGPUTestNativeSurfaceAllocator.TryCreate( - backend, width, height, - isSrgb: false, - isPremultipliedAlpha: false, out NativeSurface nativeSurface, out nint textureHandle, out nint textureViewHandle, @@ -861,12 +853,11 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( Rectangle targetBounds = new(0, 0, width, height); using DrawingCanvas canvas = - new(configuration, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), options); + new(configuration, new NativeCanvasFrame(targetBounds, nativeSurface), options); if (initialImage is not null) { Assert.True( WebGPUTestNativeSurfaceAllocator.TryWriteTexture( - backend, textureHandle, width, height, @@ -880,7 +871,6 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( Assert.True( WebGPUTestNativeSurfaceAllocator.TryReadTexture( - backend, textureHandle, width, height, @@ -1289,11 +1279,8 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using WebGPUDrawingBackend nativeSurfaceBackend = new(); Assert.True( WebGPUTestNativeSurfaceAllocator.TryCreate( - nativeSurfaceBackend, defaultImage.Width, defaultImage.Height, - isSrgb: false, - isPremultipliedAlpha: false, out NativeSurface nativeSurface, out nint textureHandle, out nint textureViewHandle, @@ -1310,7 +1297,6 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using Image initialImage = provider.GetImage(); Assert.True( WebGPUTestNativeSurfaceAllocator.TryWriteTexture( - nativeSurfaceBackend, textureHandle, defaultImage.Width, defaultImage.Height, @@ -1319,14 +1305,14 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag uploadError); using (DrawingCanvas canvas1 = - new(nativeConfig, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) + new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) { canvas1.Fill(rect1, redBrush); canvas1.Flush(); } using (DrawingCanvas canvas2 = - new(nativeConfig, new NativeSurfaceOnlyFrame(targetBounds, nativeSurface), drawingOptions)) + new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) { canvas2.Fill(rect2, blueBrush); canvas2.Flush(); @@ -1334,7 +1320,6 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag Assert.True( WebGPUTestNativeSurfaceAllocator.TryReadTexture( - nativeSurfaceBackend, textureHandle, defaultImage.Width, defaultImage.Height, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs index 324321d72..03005556a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs @@ -13,23 +13,14 @@ public void Mapper_UsesExactSilkEnumValues_ForAllSupportedFormats() { (WebGPUTextureFormatId Drawing, TextureFormat Silk)[] mappings = [ - (WebGPUTextureFormatId.R8Unorm, TextureFormat.R8Unorm), - (WebGPUTextureFormatId.RG8Unorm, TextureFormat.RG8Unorm), - (WebGPUTextureFormatId.RG8Snorm, TextureFormat.RG8Snorm), - (WebGPUTextureFormatId.Rgba8Snorm, TextureFormat.Rgba8Snorm), - (WebGPUTextureFormatId.R16Float, TextureFormat.R16float), - (WebGPUTextureFormatId.RG16Float, TextureFormat.RG16float), - (WebGPUTextureFormatId.Rgba16Float, TextureFormat.Rgba16float), - (WebGPUTextureFormatId.RG16Sint, TextureFormat.RG16Sint), - (WebGPUTextureFormatId.Rgba16Sint, TextureFormat.Rgba16Sint), - (WebGPUTextureFormatId.Rgb10A2Unorm, TextureFormat.Rgb10A2Unorm), (WebGPUTextureFormatId.Rgba8Unorm, TextureFormat.Rgba8Unorm), + (WebGPUTextureFormatId.Rgba8Snorm, TextureFormat.Rgba8Snorm), + (WebGPUTextureFormatId.Rgba8Uint, TextureFormat.Rgba8Uint), (WebGPUTextureFormatId.Bgra8Unorm, TextureFormat.Bgra8Unorm), - (WebGPUTextureFormatId.Rgba32Float, TextureFormat.Rgba32float), - (WebGPUTextureFormatId.R16Uint, TextureFormat.R16Uint), - (WebGPUTextureFormatId.RG16Uint, TextureFormat.RG16Uint), (WebGPUTextureFormatId.Rgba16Uint, TextureFormat.Rgba16Uint), - (WebGPUTextureFormatId.Rgba8Uint, TextureFormat.Rgba8Uint) + (WebGPUTextureFormatId.Rgba16Sint, TextureFormat.Rgba16Sint), + (WebGPUTextureFormatId.Rgba16Float, TextureFormat.Rgba16float), + (WebGPUTextureFormatId.Rgba32Float, TextureFormat.Rgba32float) ]; Assert.Equal(Enum.GetValues().Length, mappings.Length); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index a22a960df..57495f7c5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -43,7 +43,7 @@ public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference(T IPath pixelatePath = CreatePixelateTrianglePath(); Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); - CpuCanvasFrame proxyFrame = new(targetRegion); + MemoryCanvasFrame proxyFrame = new(targetRegion); MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame, target); NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); @@ -52,7 +52,7 @@ public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference(T using (DrawingCanvas canvas = new( configuration, - new NativeSurfaceOnlyFrame(target.Bounds, nativeSurface), + new NativeCanvasFrame(target.Bounds, nativeSurface), new DrawingOptions())) { DrawProcessScenario(canvas); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs deleted file mode 100644 index 48a94aa69..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/NativeSurfaceOnlyFrame{TPixel}.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -/// -/// Test frame wrapper that exposes only a native surface. -/// -/// The pixel format. -internal sealed class NativeSurfaceOnlyFrame : ICanvasFrame - where TPixel : unmanaged, IPixel -{ - private readonly NativeSurface surface; - - public NativeSurfaceOnlyFrame(Rectangle bounds, NativeSurface surface) - { - this.Bounds = bounds; - this.surface = surface; - } - - public Rectangle Bounds { get; } - - public bool TryGetCpuRegion(out Buffer2DRegion region) - { - region = default; - return false; - } - - public bool TryGetNativeSurface(out NativeSurface surface) - { - surface = this.surface; - return true; - } -} From b04a0ec23068feb107a0f080308a7aa06a6d6088 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 18:51:27 +1000 Subject: [PATCH 105/136] Remove NormalizeOutput --- .../PolygonGeometry/StrokedShapeGenerator.cs | 1 - .../Processing/DrawingCanvas{TPixel}.cs | 10 ++++------ .../Processing/StrokeOptions.cs | 12 ------------ .../Drawing/DrawPolygon.cs | 14 +------------- .../DrawingCanvasTests.StrokeOptions.cs | 16 +++++----------- 5 files changed, 10 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs index a37f427cd..d51d25532 100644 --- a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -146,7 +146,6 @@ private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions op ArcDetailScale = options.ArcDetailScale, MiterLimit = options.MiterLimit, InnerMiterLimit = options.InnerMiterLimit, - NormalizeOutput = options.NormalizeOutput, LineJoin = options.LineJoin switch { LineJoin.MiterRound => PolygonClipper.LineJoin.MiterRound, diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index e908c1f4f..f92462b2f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -469,9 +469,8 @@ public void Draw(Pen pen, IPath path) IPath outline = pen.GeneratePath(transformedPath); - // Non-normalized stroke output can self-overlap; non-zero winding preserves stroke semantics. - if (!pen.StrokeOptions.NormalizeOutput && - effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) { ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); shapeOptions.IntersectionRule = IntersectionRule.NonZero; @@ -1038,9 +1037,8 @@ private CompositionCommand CreateCompositionCommand( compositionPath = pen.GeneratePath(operation.Path); samplingOrigin = RasterizerSamplingOrigin.PixelCenter; - // Keep draw semantics aligned with DrawPath: non-normalized stroke output - // requires non-zero winding to preserve stroke interior behavior. - if (!pen.StrokeOptions.NormalizeOutput && intersectionRule != IntersectionRule.NonZero) + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (intersectionRule != IntersectionRule.NonZero) { intersectionRule = IntersectionRule.NonZero; } diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs index 51886f915..6430d9fbd 100644 --- a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -8,16 +8,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class StrokeOptions : IEquatable { - /// - /// Gets or sets a value indicating whether stroked contours should be normalized by - /// resolving self-intersections and overlaps before returning. - /// - /// - /// Defaults to for maximum throughput. - /// When disabled, callers should rasterize with a non-zero winding fill rule. - /// - public bool NormalizeOutput { get; set; } - /// /// Gets or sets the miter limit used to clamp outer miter joins. /// @@ -56,7 +46,6 @@ public sealed class StrokeOptions : IEquatable /// public bool Equals(StrokeOptions? other) => other is not null && - this.NormalizeOutput == other.NormalizeOutput && this.MiterLimit == other.MiterLimit && this.InnerMiterLimit == other.InnerMiterLimit && this.ArcDetailScale == other.ArcDetailScale && @@ -67,7 +56,6 @@ public bool Equals(StrokeOptions? other) /// public override int GetHashCode() => HashCode.Combine( - this.NormalizeOutput, this.MiterLimit, this.InnerMiterLimit, this.ArcDetailScale, diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 54d54d414..e01f01d8b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -208,19 +208,7 @@ public void ImageSharpCombinedPathsWebGPUNativeSurface() } [Benchmark] - public IPath ImageSharpStrokeAndClipCombined() => this.isPen.GeneratePath(this.imageSharpPath); - - [Benchmark] - public IPath ImageSharpStrokeAndClipSeparate() - { - IPath path = Path.Empty; - foreach (PointF[] loop in this.points) - { - path = this.isPen.GeneratePath(new Polygon(loop)); - } - - return path; - } + public IPath ImageSharpStroke() => this.isPen.GeneratePath(this.imageSharpPath); [Benchmark] public void FillPolygon() diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs index 142c4ea1a..c23d3aa99 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -20,15 +20,9 @@ public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider IPath leftPath = CreateBowTiePath(new RectangleF(28, 34, 128, 152)); IPath rightPath = CreateBowTiePath(new RectangleF(204, 34, 128, 152)); - SolidPen nonNormalizedPen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); - nonNormalizedPen.StrokeOptions.NormalizeOutput = false; - nonNormalizedPen.StrokeOptions.LineJoin = LineJoin.Round; - nonNormalizedPen.StrokeOptions.LineCap = LineCap.Round; - - SolidPen normalizedPen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); - normalizedPen.StrokeOptions.NormalizeOutput = true; - normalizedPen.StrokeOptions.LineJoin = LineJoin.Round; - normalizedPen.StrokeOptions.LineCap = LineCap.Round; + SolidPen pen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); + pen.StrokeOptions.LineJoin = LineJoin.Round; + pen.StrokeOptions.LineCap = LineCap.Round; DrawingOptions evenOddOptions = new() { @@ -39,8 +33,8 @@ public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider canvas.Fill(new Rectangle(12, 12, 336, 196), Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F))); _ = canvas.Save(evenOddOptions); - canvas.Draw(nonNormalizedPen, leftPath); - canvas.Draw(normalizedPen, rightPath); + canvas.Draw(pen, leftPath); + canvas.Draw(pen, rightPath); canvas.Restore(); canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2F), leftPath); From 2f0a7f654f4f40812fc339d5bbeb6d0baae9524a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 22:32:41 +1000 Subject: [PATCH 106/136] Move stroking to the GPU --- .../DashPathSplitter.cs | 155 +++++++ .../Shaders/CompositeComputeShader.cs | 224 ++++++++-- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 409 +++++++++++++++++- .../WebGPUDrawingBackend.cs | 154 ++++++- .../Processing/Backends/CompositionBatch.cs | 4 +- .../Processing/Backends/CompositionCommand.cs | 107 ++++- .../Backends/CompositionCoverageDefinition.cs | 62 ++- .../Backends/CompositionScenePlanner.cs | 67 ++- .../Backends/CoveragePreparationMode.cs | 20 - .../Backends/DefaultDrawingBackend.cs | 46 +- .../Backends/DrawingCoverageHandle.cs | 51 --- .../Backends/PreparedCompositionCommand.cs | 20 +- .../Processing/DrawingCanvas{TPixel}.cs | 192 +++++--- .../Processing/IDrawingCanvas.cs | 1 - .../Processing/PatternPen.cs | 2 +- src/ImageSharp.Drawing/Processing/Pen.cs | 4 +- .../Backends/SkiaCoverageDrawingBackend.cs | 286 ------------ .../SkiaCoverageDrawingBackendTests.cs | 98 ----- .../Backends/WebGPUDrawingBackendTests.cs | 11 +- .../Processing/DrawingCanvasBatcherTests.cs | 91 +++- ...StateIsolation_MatchesReference_Rgba32.png | 4 +- ...izeOutputFalse_MatchesReference_Rgba32.png | 4 +- ...aw_PathBuilder_MatchesReference_Rgba32.png | 4 +- ...ndGradientPens_MatchesReference_Rgba32.png | 4 +- .../ClipOffset_offset_x-20_y-20.png | 4 +- ...ntStyleSquare_Rgba32_Yellow_A(1)_T(10).png | 4 +- .../DrawPathCircleUsingAddArc_359.png | 4 +- .../DrawPath_HotPink_A150_T5.png | 4 +- .../DrawPath_HotPink_A255_T5.png | 4 +- .../DrawPath_White_A255_T1.5.png | 4 +- ...5,255,255,255)_ColorFontsEnabled-False.png | 4 +- .../FillComplexPolygon_SolidFill.png | 4 +- .../FillComplexPolygon_SolidFill__Overlap.png | 4 +- ...lComplexPolygon_SolidFill__Transparent.png | 4 +- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 2 +- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 2 +- ...ntShapesAreRenderedCorrectly_LargeText.png | 4 +- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 +- ...oJson_Mississippi_LinesScaled_Scale(3).png | 4 +- ...oJson_Mississippi_LinesScaled_Scale(5).png | 2 +- ...oJson_Mississippi_Lines_PixelOffset(0).png | 2 +- ...on_Mississippi_Lines_PixelOffset(5500).png | 4 +- .../LargeGeoJson_States_Fill.png | 2 +- ...ath_Rgba32_Blank500x400_type-pie_small.png | 4 +- ...utput_DrawPath_Stroke_WebGPU_CPURegion.png | 4 +- ...t_DrawPath_Stroke_WebGPU_NativeSurface.png | 4 +- ...eCache_RepeatedGlyphs_WebGPU_CPURegion.png | 4 +- ...he_RepeatedGlyphs_WebGPU_NativeSurface.png | 4 +- ...aredCoverage_DrawText_WebGPU_CPURegion.png | 4 +- ...Coverage_DrawText_WebGPU_NativeSurface.png | 4 +- ...tput_FillPath_AliasedThreshold_Default.png | 3 + ...Path_AliasedThreshold_WebGPU_CPURegion.png | 3 + ..._AliasedThreshold_WebGPU_NativeSurface.png | 3 + ...Brush_Darken_DestAtop_WebGPU_CPURegion.png | 4 +- ...h_Darken_DestAtop_WebGPU_NativeSurface.png | 4 +- ...geBrush_HardLight_Xor_WebGPU_CPURegion.png | 4 +- ...ush_HardLight_Xor_WebGPU_NativeSurface.png | 4 +- ...eBrush_Lighten_DestIn_WebGPU_CPURegion.png | 4 +- ...sh_Lighten_DestIn_WebGPU_NativeSurface.png | 4 +- ...rush_Multiply_SrcAtop_WebGPU_CPURegion.png | 4 +- ..._Multiply_SrcAtop_WebGPU_NativeSurface.png | 4 +- ...idBrush_HardLight_Xor_WebGPU_CPURegion.png | 4 +- ...ush_HardLight_Xor_WebGPU_NativeSurface.png | 4 +- ...dBrush_Normal_SrcOver_WebGPU_CPURegion.png | 4 +- ...sh_Normal_SrcOver_WebGPU_NativeSurface.png | 4 +- ...urfaceSubregionParity_WebGPU_CPURegion.png | 4 +- ...ceSubregionParity_WebGPU_NativeSurface.png | 4 +- ...DefaultOutput_Process_WebGPU_CPURegion.png | 4 +- ...ultOutput_Process_WebGPU_NativeSurface.png | 4 +- 69 files changed, 1516 insertions(+), 671 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs delete mode 100644 src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png diff --git a/src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs b/src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs new file mode 100644 index 000000000..f12f873f5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Splits a path into dash segments without performing stroke expansion. +/// Each "on" dash segment is returned as an open sub-path. +/// +internal static class DashPathSplitter +{ + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// A path containing the "on" dash segments. + public static IPath SplitDashes(IPath path, float strokeWidth, ReadOnlySpan pattern) + { + if (pattern.Length < 2) + { + return path; + } + + const float eps = 1e-6f; + + float patternLength = 0f; + for (int i = 0; i < pattern.Length; i++) + { + patternLength += MathF.Abs(pattern[i]) * strokeWidth; + } + + if (patternLength <= eps) + { + return path; + } + + IEnumerable simplePaths = path.Flatten(); + List segments = []; + List buffer = new(64); + + foreach (ISimplePath p in simplePaths) + { + bool online = true; + int patternPos = 0; + float targetLength = pattern[patternPos] * strokeWidth; + + ReadOnlySpan pts = p.Points.Span; + if (pts.Length < 2) + { + continue; + } + + int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; + int ei = 0; + Vector2 current = pts[0]; + + while (ei < edgeCount) + { + int nextIndex = p.IsClosed ? (ei + 1) % pts.Length : ei + 1; + Vector2 next = pts[nextIndex]; + float segLen = Vector2.Distance(current, next); + + if (segLen <= eps) + { + current = next; + ei++; + continue; + } + + if (segLen + eps < targetLength) + { + if (online) + { + buffer.Add(current); + } + + current = next; + ei++; + targetLength -= segLen; + continue; + } + + if (MathF.Abs(segLen - targetLength) <= eps) + { + if (online) + { + buffer.Add(current); + buffer.Add(next); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = next; + ei++; + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + continue; + } + + float t = targetLength / segLen; + Vector2 split = current + (t * (next - current)); + + if (online) + { + buffer.Add(current); + buffer.Add(split); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = split; + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + } + + if (buffer.Count > 0) + { + if (online) + { + buffer.Add(current); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + } + } + + if (segments.Count == 0) + { + return Path.Empty; + } + + if (segments.Count == 1) + { + return segments[0]; + } + + return new ComplexPolygon(segments); + } + + private static void FlushBuffer(List buffer, List segments) + { + if (buffer.Count >= 2 && buffer[0] != buffer[^1]) + { + segments.Add(new Path(new LinearLineSegment([.. buffer]))); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 8a5b57c5e..5ffec30be 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -24,6 +24,7 @@ struct Edge { y0: i32, x1: i32, y1: i32, + flags: i32, } struct Params { @@ -53,6 +54,12 @@ struct Params { solid_a: u32, rasterization_mode: u32, antialias_threshold: u32, + stroke_mode: u32, + stroke_half_width: u32, + stroke_line_cap: u32, + stroke_line_join: u32, + stroke_miter_limit: u32, + stroke_pad0: u32, }; struct DispatchConfig { @@ -807,6 +814,148 @@ fn area_to_coverage(area_val: i32, fill_rule: u32, rasterization_mode: u32, anti return coverage; } + // ----------------------------------------------------------------------- + // Stroke distance-field helpers + // ----------------------------------------------------------------------- + + // Returns vec2(squared_distance, unclamped_t) from point p to line segment a→b. + // The returned distance uses clamped t (0..1), but the unclamped t is also returned + // so callers can detect endpoint proximity for cap handling. + fn dist_to_segment(p: vec2, a: vec2, b: vec2) -> vec2 { + let ab = b - a; + let ap = p - a; + let len_sq = dot(ab, ab); + var unclamped_t: f32; + if len_sq < 1e-10 { + unclamped_t = 0.0; + } else { + unclamped_t = dot(ap, ab) / len_sq; + } + let t = clamp(unclamped_t, 0.0, 1.0); + let closest = a + ab * t; + let d = p - closest; + return vec2(dot(d, d), unclamped_t); + } + + // Edge flags (matches C# GpuEdge.Flags bit layout). + const EDGE_OPEN_START: i32 = 1; // (x0,y0) is an open path start + const EDGE_OPEN_END: i32 = 2; // (x1,y1) is an open path end + + // LineCap enum values. + const CAP_BUTT: u32 = 0u; + const CAP_SQUARE: u32 = 1u; + const CAP_ROUND: u32 = 2u; + + // Computes stroke coverage for a pixel using distance-field evaluation. + // Iterates all edges in relevant bands, finds min distance to centerline, + // applies line cap rules using edge flags, and returns antialiased coverage. + // + // Line join handling: + // Miter/MiterRevert/MiterRound joins are handled by extending centerline + // segments past interior vertices on the CPU side. The distance field then + // naturally produces the correct miter coverage. + // Round joins are the natural distance-field behavior. + // Bevel join coverage is very close to round and accepted as-is. + fn stroke_coverage( + dest_x_i32: i32, + dest_y_i32: i32, + command: Params, + half_width: f32, + line_cap: u32, + line_join: u32, + miter_limit: f32, + tile_min_x: i32, + tile_min_y: i32, + ) -> f32 { + let px = f32(dest_x_i32 - command.edge_origin_x) + 0.5; + let py = f32(dest_y_i32 - command.edge_origin_y) + 0.5; + let point = vec2(px, py); + + // Determine band range for this pixel. Edges are stored per 16-row band. + // We must check all bands whose edges could be within half_width + 1 distance. + let expand = i32(ceil(half_width)) + 1; + let pixel_y = i32(py); + var first_band = max((pixel_y - expand) / 16, 0); + if (pixel_y - expand) < 0 && ((pixel_y - expand) % 16) != 0 { + first_band = max(first_band - 1, 0); + } + let last_band = min((pixel_y + expand) / 16, i32(command.csr_band_count) - 1); + if first_band > last_band || i32(command.csr_band_count) == 0 { + return 0.0; + } + + let sentinel = half_width * half_width + half_width * 2.0 + 1.0; + var min_dist_sq = sentinel; + let inv_fixed = 1.0 / f32(FIXED_ONE); + + for (var band = first_band; band <= last_band; band++) { + let b_start = band_offsets[command.csr_offsets_start + u32(band)]; + let b_end = band_offsets[command.csr_offsets_start + u32(band) + 1u]; + for (var ei = b_start; ei < b_end; ei++) { + let edge = edges[command.edge_start + ei]; + let a = vec2(f32(edge.x0) * inv_fixed, f32(edge.y0) * inv_fixed); + let b = vec2(f32(edge.x1) * inv_fixed, f32(edge.y1) * inv_fixed); + let flags = edge.flags; + let is_open_start = (flags & EDGE_OPEN_START) != 0; + let is_open_end = (flags & EDGE_OPEN_END) != 0; + + let result = dist_to_segment(point, a, b); + var d_sq = result.x; + let unclamped_t = result.y; + + // Cap handling at open path endpoints. + // Interior vertices (no open flags) use the natural clamped distance, + // which produces smooth coverage where adjacent segments meet. + if line_cap == CAP_BUTT { + // Butt cap: no coverage past the open endpoint. + // Exclude this edge if the pixel projects past an open end. + if (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end) { + d_sq = sentinel; + } + } else if line_cap == CAP_SQUARE { + // Square cap: extend the segment by half_width at open endpoints only. + let needs_ext = (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end); + if needs_ext { + let seg = b - a; + let seg_len = length(seg); + if seg_len > 1e-6 { + let dir = seg / seg_len; + var ext_a = a; + var ext_b = b; + if is_open_start { + ext_a = a - dir * half_width; + } + if is_open_end { + ext_b = b + dir * half_width; + } + let ext_result = dist_to_segment(point, ext_a, ext_b); + d_sq = ext_result.x; + } + } + // For non-open endpoints or when projection is on-segment, + // use the natural clamped distance (d_sq unchanged). + } + // CAP_ROUND: natural clamped distance produces round caps. No change needed. + + min_dist_sq = min(min_dist_sq, d_sq); + } + } + + let min_dist = sqrt(min_dist_sq); + + // Antialiased coverage. + let rasterization_mode = command.rasterization_mode; + if rasterization_mode == 1u { + // Aliased mode. + if min_dist <= half_width { + return 1.0; + } + return 0.0; + } + // Antialiased: smooth transition over 1 pixel at boundary. + return clamp(half_width + 0.5 - min_dist, 0.0, 1.0); + } + // ----------------------------------------------------------------------- // Main entry point // ----------------------------------------------------------------------- @@ -862,6 +1011,25 @@ fn cs_main( continue; } + // Branch: stroke mode vs fill mode. + let is_stroke = command.stroke_mode == 1u; + + var coverage_value = 0.0; + if is_stroke { + // Stroke path: per-pixel distance-field evaluation. + if in_bounds && dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { + let half_width = u32_to_f32(command.stroke_half_width); + coverage_value = stroke_coverage( + dest_x_i32, dest_y_i32, command, + half_width, + command.stroke_line_cap, + command.stroke_line_join, + u32_to_f32(command.stroke_miter_limit), + tile_min_x, tile_min_y); + } + } else { + // Fill path: scanline rasterizer. + // Determine this tile's position in coverage-local space. let band_top = tile_min_y - command.edge_origin_y; let band_left_fixed = (tile_min_x - command.edge_origin_x) << FIXED_SHIFT; @@ -925,7 +1093,7 @@ fn cs_main( } workgroupBarrier(); - // Compute coverage and compose for this pixel. + // Compute coverage for fill. if in_bounds { if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { var cover = atomicLoad(&tile_start_cover[py]); @@ -933,33 +1101,37 @@ fn cs_main( cover += atomicLoad(&tile_cover[py * 16u + col]); } let area_val = atomicLoad(&tile_area[py * 16u + px]) + (cover << AREA_SHIFT); - let coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold)); - - if coverage_value > 0.0 { - let blend_percentage = u32_to_f32(command.blend_percentage); - let effective_coverage = coverage_value * blend_percentage; - - var brush = vec4( - u32_to_f32(command.solid_r), - u32_to_f32(command.solid_g), - u32_to_f32(command.solid_b), - u32_to_f32(command.solid_a)); - - if command.brush_type == 1u { - let origin_x = bitcast(command.brush_origin_x); - let origin_y = bitcast(command.brush_origin_y); - let region_x = i32(command.brush_region_x); - let region_y = i32(command.brush_region_y); - let region_w = i32(command.brush_region_width); - let region_h = i32(command.brush_region_height); - let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; - let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; - brush = __LOAD_BRUSH__; - } + coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold)); + } + } + } // end fill path - let src = vec4(brush.rgb, brush.a * effective_coverage); - destination = compose_pixel(destination, src, command.color_blend_mode, command.alpha_composition_mode); + // Compose coverage result (shared by fill and stroke paths). + if in_bounds && coverage_value > 0.0 { + if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { + let blend_percentage = u32_to_f32(command.blend_percentage); + let effective_coverage = coverage_value * blend_percentage; + + var brush = vec4( + u32_to_f32(command.solid_r), + u32_to_f32(command.solid_g), + u32_to_f32(command.solid_b), + u32_to_f32(command.solid_a)); + + if command.brush_type == 1u { + let origin_x = bitcast(command.brush_origin_x); + let origin_y = bitcast(command.brush_origin_y); + let region_x = i32(command.brush_region_x); + let region_y = i32(command.brush_region_y); + let region_w = i32(command.brush_region_width); + let region_h = i32(command.brush_region_height); + let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; + let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; + brush = __LOAD_BRUSH__; } + + let src = vec4(brush.rgb, brush.a * effective_coverage); + destination = compose_pixel(destination, src, command.color_blend_mode, command.alpha_composition_mode); } } workgroupBarrier(); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 957724da3..140718a4c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; public sealed unsafe partial class WebGPUDrawingBackend { private const int TileHeight = 16; - private const int EdgeStrideBytes = 16; + private const int EdgeStrideBytes = 20; private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private const int CsrWorkgroupSize = 256; @@ -84,15 +84,46 @@ private bool TryCreateEdgeBuffer( uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); - if (!TryBuildFixedPointEdges( + // For stroke definitions, expand band assignment so distance-field + // lookups can find nearby edges beyond the edge's own Y range. + int strokeExpand = definition.IsStroke + ? (int)MathF.Ceiling(definition.StrokeWidth * 0.5f) + 1 + : 0; + + IMemoryOwner? defEdgeOwner; + int edgeCount; + uint[]? defBandOffsets; + bool edgeSuccess; + if (definition.IsStroke) + { + edgeSuccess = TryBuildStrokeEdges( + flushContext.MemoryAllocator, + definition.Path, + in interest, + definition.RasterizerOptions.SamplingOrigin, + definition.StrokeWidth * 0.5f, + definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel, + (float)(definition.StrokeOptions?.MiterLimit ?? 4.0), + out defEdgeOwner, + out edgeCount, + out defBandOffsets, + out error, + strokeExpand); + } + else + { + edgeSuccess = TryBuildFixedPointEdges( flushContext.MemoryAllocator, definition.Path, in interest, definition.RasterizerOptions.SamplingOrigin, - out IMemoryOwner? defEdgeOwner, - out int edgeCount, - out uint[]? defBandOffsets, - out error)) + out defEdgeOwner, + out edgeCount, + out defBandOffsets, + out error); + } + + if (!edgeSuccess) { // Dispose any already-built geometry on failure. for (int j = 0; j < i; j++) @@ -106,12 +137,21 @@ private bool TryCreateEdgeBuffer( geometries[i] = new DefinitionGeometry(defEdgeOwner, edgeCount, bandCount, defBandOffsets); int bandOffsetEntriesForDef = bandCount + 1; + uint strokeLineCap = definition.IsStroke ? (uint)(definition.StrokeOptions?.LineCap ?? LineCap.Butt) : 0; + uint strokeLineJoin = definition.IsStroke ? (uint)(definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel) : 0; + float strokeMiterLimit = definition.IsStroke ? (float)(definition.StrokeOptions?.MiterLimit ?? 4.0) : 0f; + edgePlacements[i] = new EdgePlacement( (uint)runningEdgeStart, (uint)edgeCount, fillRule, (uint)runningBandOffset, - (uint)bandCount); + (uint)bandCount, + definition.IsStroke, + definition.StrokeWidth, + strokeLineCap, + strokeLineJoin, + strokeMiterLimit); runningEdgeStart += edgeCount; runningBandOffset += bandOffsetEntriesForDef; @@ -449,6 +489,7 @@ private static bool TryBuildFixedPointEdges( int yMaxFixed = Math.Max(y0, y1); int minRow = Math.Max(0, yMinFixed >> FixedShift); int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); + if (minRow > maxRow) { continue; @@ -517,6 +558,7 @@ private static bool TryBuildFixedPointEdges( int yMaxFixed = Math.Max(y0, y1); int minRow = Math.Max(0, yMinFixed >> FixedShift); int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); + if (minRow > maxRow) { continue; @@ -539,6 +581,352 @@ private static bool TryBuildFixedPointEdges( return true; } + /// + /// Builds stroke-specific fixed-point edge geometry with cap flags and miter extensions. + /// Unlike the fill edge builder, this includes horizontal edges and computes + /// per-vertex miter extensions for Miter/MiterRevert/MiterRound join types. + /// + private static bool TryBuildStrokeEdges( + MemoryAllocator allocator, + IPath path, + in Rectangle interest, + RasterizerSamplingOrigin samplingOrigin, + float halfWidth, + LineJoin lineJoin, + float miterLimit, + out IMemoryOwner? edgeOwner, + out int edgeCount, + out uint[]? bandOffsets, + out string? error, + int strokeExpandPixels = 0) + { + error = null; + edgeOwner = null; + edgeCount = 0; + bandOffsets = null; + bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + int height = interest.Height; + int interestX = interest.X; + int interestY = interest.Y; + int bandCount = (int)DivideRoundUp(height, TileHeight); + bool isMiterJoin = lineJoin is LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound; + + // Pre-process: flatten all sub-paths and compute stroke edges with + // miter extensions and endpoint flags. + List strokeEdges = []; + + foreach (ISimplePath simplePath in path.Flatten()) + { + ReadOnlySpan points = simplePath.Points.Span; + if (points.Length < 2) + { + continue; + } + + bool isClosed = simplePath.IsClosed; + int segmentCount = isClosed ? points.Length : points.Length - 1; + if (segmentCount == 0) + { + continue; + } + + // Pre-compute per-vertex miter extensions. + // extensions[j] is the amount to extend the outgoing segment backward + // (and the incoming segment forward) at vertex j. + float[] extensions = new float[points.Length]; + if (isMiterJoin) + { + ComputeMiterExtensions(points, isClosed, halfWidth, miterLimit, lineJoin, extensions); + } + + for (int j = 0; j < segmentCount; j++) + { + int j1 = j + 1 == points.Length ? 0 : j + 1; + PointF p0 = points[j]; + PointF p1 = points[j1]; + + // Apply miter extensions at both endpoints. + float ext0 = extensions[j]; // extension at start vertex (forward along this segment) + float ext1 = extensions[j1]; // extension at end vertex (backward along this segment) + + if (ext0 > 0f || ext1 > 0f) + { + float dx = p1.X - p0.X; + float dy = p1.Y - p0.Y; + float segLen = MathF.Sqrt((dx * dx) + (dy * dy)); + if (segLen > 1e-6f) + { + float invLen = 1f / segLen; + float dirX = dx * invLen; + float dirY = dy * invLen; + + // Extend start backward (away from p1). + if (ext0 > 0f) + { + p0 = new PointF(p0.X - (dirX * ext0), p0.Y - (dirY * ext0)); + } + + // Extend end forward (away from p0). + if (ext1 > 0f) + { + p1 = new PointF(p1.X + (dirX * ext1), p1.Y + (dirY * ext1)); + } + } + } + + // Compute cap flags. + int flags = 0; + if (!isClosed) + { + if (j == 0) + { + flags |= 1; // open start at (x0, y0) + } + + if (j == segmentCount - 1) + { + flags |= 2; // open end at (x1, y1) + } + } + + float fx0 = (p0.X - interestX) + samplingOffsetX; + float fy0 = (p0.Y - interestY) + samplingOffsetY; + float fx1 = (p1.X - interestX) + samplingOffsetX; + float fy1 = (p1.Y - interestY) + samplingOffsetY; + + int x0 = (int)MathF.Round(fx0 * FixedOne); + int y0 = (int)MathF.Round(fy0 * FixedOne); + int x1 = (int)MathF.Round(fx1 * FixedOne); + int y1 = (int)MathF.Round(fy1 * FixedOne); + + strokeEdges.Add(new GpuEdge { X0 = x0, Y0 = y0, X1 = x1, Y1 = y1, Flags = flags }); + } + } + + if (strokeEdges.Count == 0) + { + return true; + } + + // Count edges per band (including horizontal edges, with stroke expansion). + int[] bandCounts = new int[bandCount]; + int totalSubEdges = 0; + + for (int i = 0; i < strokeEdges.Count; i++) + { + GpuEdge edge = strokeEdges[i]; + ComputeStrokeBandRange(edge, height, strokeExpandPixels, out int minBand, out int maxBand); + if (minBand > maxBand) + { + continue; + } + + for (int b = minBand; b <= maxBand; b++) + { + bandCounts[b]++; + } + + totalSubEdges += maxBand - minBand + 1; + } + + if (totalSubEdges == 0) + { + return true; + } + + // Prefix sum → band offsets. + uint[] offsets = new uint[bandCount + 1]; + uint running = 0; + for (int b = 0; b < bandCount; b++) + { + offsets[b] = running; + running += (uint)bandCounts[b]; + } + + offsets[bandCount] = running; + + // Scatter edges into band-sorted buffer. + IMemoryOwner finalOwner = allocator.Allocate(totalSubEdges); + Span finalSpan = finalOwner.Memory.Span; + uint[] writeCursors = new uint[bandCount]; + + for (int i = 0; i < strokeEdges.Count; i++) + { + GpuEdge edge = strokeEdges[i]; + ComputeStrokeBandRange(edge, height, strokeExpandPixels, out int minBand, out int maxBand); + if (minBand > maxBand) + { + continue; + } + + for (int band = minBand; band <= maxBand; band++) + { + finalSpan[(int)(offsets[band] + writeCursors[band])] = edge; + writeCursors[band]++; + } + } + + edgeOwner = finalOwner; + edgeCount = totalSubEdges; + bandOffsets = offsets; + return true; + } + + /// + /// Computes band range for a stroke edge, including horizontal edges and stroke expansion. + /// + private static void ComputeStrokeBandRange(in GpuEdge edge, int height, int strokeExpandPixels, out int minBand, out int maxBand) + { + int y0 = edge.Y0; + int y1 = edge.Y1; + + int yMinFixed, yMaxFixed; + if (y0 == y1) + { + // Horizontal edge: use the row at this Y position. + yMinFixed = y0; + yMaxFixed = y0 + 1; // ensure at least one row + } + else + { + yMinFixed = Math.Min(y0, y1); + yMaxFixed = Math.Max(y0, y1); + } + + int minRow = Math.Max(0, yMinFixed >> FixedShift); + int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); + + // For horizontal edges the row range can be empty; use the Y row directly. + if (y0 == y1) + { + int row = Math.Clamp(y0 >> FixedShift, 0, height - 1); + minRow = row; + maxRow = row; + } + + if (strokeExpandPixels > 0) + { + minRow = Math.Max(0, minRow - strokeExpandPixels); + maxRow = Math.Min(height - 1, maxRow + strokeExpandPixels); + } + + if (minRow > maxRow) + { + minBand = 1; + maxBand = 0; // empty range sentinel + return; + } + + minBand = minRow / TileHeight; + maxBand = maxRow / TileHeight; + } + + /// + /// Pre-computes miter extension lengths at each vertex of a sub-path. + /// At each interior vertex where two segments meet, the extension is + /// halfWidth / tan(halfAngle), clamped by the miter limit. + /// + private static void ComputeMiterExtensions( + ReadOnlySpan points, + bool isClosed, + float halfWidth, + float miterLimit, + LineJoin lineJoin, + float[] extensions) + { + int n = points.Length; + + for (int i = 0; i < n; i++) + { + extensions[i] = 0f; + } + + // For each interior vertex, compute the miter extension. + int startVertex = isClosed ? 0 : 1; + int endVertex = isClosed ? n : n - 1; + float limit = halfWidth * miterLimit; + + for (int i = startVertex; i < endVertex; i++) + { + int prev = isClosed ? (i - 1 + n) % n : i - 1; + int next = isClosed ? (i + 1) % n : i + 1; + + float dx1 = points[i].X - points[prev].X; + float dy1 = points[i].Y - points[prev].Y; + float dx2 = points[next].X - points[i].X; + float dy2 = points[next].Y - points[i].Y; + + float len1 = MathF.Sqrt((dx1 * dx1) + (dy1 * dy1)); + float len2 = MathF.Sqrt((dx2 * dx2) + (dy2 * dy2)); + if (len1 < 1e-6f || len2 < 1e-6f) + { + continue; + } + + // Normalize directions. + float ux1 = dx1 / len1; + float uy1 = dy1 / len1; + float ux2 = dx2 / len2; + float uy2 = dy2 / len2; + + // Dot product of directions gives cos(angle). + float dot = (ux1 * ux2) + (uy1 * uy2); + dot = Math.Clamp(dot, -1f, 1f); + + // Half-angle: cos(θ) = dot → θ = acos(dot) → half = θ/2. + float angle = MathF.Acos(dot); + float halfAngle = angle * 0.5f; + float sinHalf = MathF.Sin(halfAngle); + + if (sinHalf < 1e-6f) + { + // Near-parallel segments (straight line), no miter needed. + continue; + } + + float cosHalf = MathF.Cos(halfAngle); + float tanHalf = sinHalf / cosHalf; + if (tanHalf < 1e-6f) + { + continue; + } + + // Full extension along each segment = halfWidth / tan(halfAngle). + float ext = halfWidth / tanHalf; + + // Miter distance from vertex = halfWidth / sin(halfAngle). + float miterDistance = halfWidth / sinHalf; + + if (miterDistance <= limit) + { + // Within miter limit: full extension for all join types. + extensions[i] = ext; + } + else + { + // Miter limit exceeded: behavior depends on join type. + switch (lineJoin) + { + case LineJoin.Miter: + // Clip the miter at the limit distance. + // The clipped extension is the projection of the clipped point onto the segment. + extensions[i] = limit * cosHalf; + break; + + case LineJoin.MiterRevert: + // Bevel fallback: no extension needed. + break; + + case LineJoin.MiterRound: + // Round fallback: natural SDF handles it, no extension needed. + break; + } + } + } + } + /// /// Creates and executes a compute pass for a coverage pipeline stage. /// @@ -792,6 +1180,13 @@ private struct GpuEdge public int Y0; public int X1; public int Y1; + + /// + /// Bit flags for stroke edge metadata. + /// Bit 0: open start — the (X0,Y0) endpoint is an open path start (cap applies). + /// Bit 1: open end — the (X1,Y1) endpoint is an open path end (cap applies). + /// + public int Flags; } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 3d8642fc8..723b57b86 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -163,7 +163,7 @@ public void FlushCompositions( for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) { CompositionBatch batch = preparedBatches[batchIndex]; - IReadOnlyList commands = batch.Commands; + List commands = batch.Commands; for (int i = 0; i < commands.Count; i++) { Rectangle destination = Rectangle.Intersect(commands[i].DestinationRegion, targetExtent); @@ -240,9 +240,10 @@ public void FlushCompositions( target.Bounds, compositionBounds.Value, commandCount, + out Rectangle effectiveBounds, out failure); - bool finalizeOk = renderOk && this.TryFinalizeFlush(flushContext, cpuRegion, compositionBounds); + bool finalizeOk = renderOk && this.TryFinalizeFlush(flushContext, cpuRegion, effectiveBounds); gpuSuccess = finalizeOk; } @@ -372,9 +373,11 @@ private bool TryRenderPreparedFlush( Rectangle targetBounds, Rectangle compositionBounds, int commandCount, + out Rectangle effectiveCompositionBounds, out string? error) where TPixel : unmanaged, IPixel { + effectiveCompositionBounds = compositionBounds; Rectangle targetLocalBounds = Rectangle.Intersect( new Rectangle(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height), compositionBounds); @@ -427,7 +430,7 @@ private bool TryRenderPreparedFlush( for (int i = 0; i < preparedBatches.Count; i++) { CompositionBatch batch = preparedBatches[i]; - IReadOnlyList commands = batch.Commands; + List commands = batch.Commands; if (commands.Count == 0) { continue; @@ -451,6 +454,96 @@ private bool TryRenderPreparedFlush( return true; } + // Prepare stroke definitions for GPU distance-field evaluation. + // Instead of expanding to a filled outline on the CPU, we compute + // the interest rectangle from the centerline bounds inflated by + // half the stroke width and pass the centerline edges to the GPU. + // For dashed strokes, we pre-split the path into dash segments + // on the CPU (cheap) while keeping the actual stroke coverage on GPU. + for (int i = 0; i < coverageDefinitions.Count; i++) + { + CompositionCoverageDefinition definition = coverageDefinitions[i]; + if (!definition.IsStroke) + { + continue; + } + + IPath strokePath = definition.Path; + + // For dashed strokes, split the path into dash segments. + // This reuses the outline generation with a minimal width to + // produce the dash-split centerline path, but instead we use + // the dedicated dash splitting API. + if (definition.StrokePattern.Length > 0) + { + // For dashed strokes, split the path into dash segments on the CPU + // so the GPU evaluates solid strokes on each dash segment. + strokePath = DashPathSplitter.SplitDashes(strokePath, definition.StrokeWidth, definition.StrokePattern.Span); + } + + float halfWidth = definition.StrokeWidth * 0.5f; + float maxExtent = halfWidth * Math.Max((float)(definition.StrokeOptions?.MiterLimit ?? 4.0), 1.0f); + + RectangleF pathBounds = strokePath.Bounds; + pathBounds = new RectangleF( + pathBounds.X + 0.5F - maxExtent, + pathBounds.Y + 0.5F - maxExtent, + pathBounds.Width + (maxExtent * 2), + pathBounds.Height + (maxExtent * 2)); + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(pathBounds.Left), + (int)MathF.Floor(pathBounds.Top), + (int)MathF.Ceiling(pathBounds.Right), + (int)MathF.Ceiling(pathBounds.Bottom)); + + RasterizerOptions opts = definition.RasterizerOptions; + coverageDefinitions[i] = new CompositionCoverageDefinition( + definition.DefinitionKey, + strokePath, + new RasterizerOptions(interest, opts.IntersectionRule, opts.RasterizationMode, opts.SamplingOrigin, opts.AntialiasThreshold), + definition.DestinationOffset, + definition.StrokeOptions, + definition.StrokeWidth, + definition.StrokePattern); + + // Re-prepare all batches that reference this coverage definition. + for (int b = 0; b < preparedBatches.Count; b++) + { + if (batchCoverageIndices[b] == i) + { + CompositionScenePlanner.ReprepareBatchCommands( + preparedBatches[b].Commands, + targetBounds, + interest); + } + } + } + + // Recompute effective composition bounds from updated command destinations + // after stroke re-preparation tightened the interest rectangles. + Rectangle targetExtent = new(0, 0, flushContext.TargetBounds.Width, flushContext.TargetBounds.Height); + Rectangle? tightBounds = null; + for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) + { + List cmds = preparedBatches[batchIndex].Commands; + for (int i = 0; i < cmds.Count; i++) + { + Rectangle destination = Rectangle.Intersect(cmds[i].DestinationRegion, targetExtent); + if (destination.Width > 0 && destination.Height > 0) + { + tightBounds = tightBounds.HasValue + ? Rectangle.Union(tightBounds.Value, destination) + : destination; + } + } + } + + if (tightBounds.HasValue) + { + effectiveCompositionBounds = tightBounds.Value; + } + if (!this.TryCreateEdgeBuffer( flushContext, coverageDefinitions, @@ -602,7 +695,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out continue; } - IReadOnlyList commands = preparedBatches[batchIndex].Commands; + List commands = preparedBatches[batchIndex].Commands; for (int i = 0; i < commands.Count; i++) { PreparedCompositionCommand command = commands[i]; @@ -697,7 +790,12 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out command.GraphicsOptions.BlendPercentage, solidColor, command.GraphicsOptions.Antialias ? 0u : 1u, - command.GraphicsOptions.AntialiasThreshold); + command.GraphicsOptions.AntialiasThreshold, + edgePlacement.IsStroke ? 1u : 0u, + edgePlacement.StrokeWidth * 0.5f, + edgePlacement.StrokeLineCap, + edgePlacement.StrokeLineJoin, + edgePlacement.StrokeMiterLimit); parameters[commandIndex] = commandParameters; commandIndex++; @@ -1632,13 +1730,28 @@ public override int GetHashCode() private readonly struct EdgePlacement { - public EdgePlacement(uint edgeStart, uint edgeCount, uint fillRule, uint csrOffsetsStart, uint csrBandCount) + public EdgePlacement( + uint edgeStart, + uint edgeCount, + uint fillRule, + uint csrOffsetsStart, + uint csrBandCount, + bool isStroke = false, + float strokeWidth = 0f, + uint strokeLineCap = 0, + uint strokeLineJoin = 0, + float strokeMiterLimit = 0f) { this.EdgeStart = edgeStart; this.EdgeCount = edgeCount; this.FillRule = fillRule; this.CsrOffsetsStart = csrOffsetsStart; this.CsrBandCount = csrBandCount; + this.IsStroke = isStroke; + this.StrokeWidth = strokeWidth; + this.StrokeLineCap = strokeLineCap; + this.StrokeLineJoin = strokeLineJoin; + this.StrokeMiterLimit = strokeMiterLimit; } public uint EdgeStart { get; } @@ -1650,6 +1763,16 @@ public EdgePlacement(uint edgeStart, uint edgeCount, uint fillRule, uint csrOffs public uint CsrOffsetsStart { get; } public uint CsrBandCount { get; } + + public bool IsStroke { get; } + + public float StrokeWidth { get; } + + public uint StrokeLineCap { get; } + + public uint StrokeLineJoin { get; } + + public float StrokeMiterLimit { get; } } /// @@ -1745,6 +1868,12 @@ private readonly struct PreparedCompositeParameters public readonly uint SolidA; public readonly uint RasterizationMode; public readonly uint AntialiasThreshold; + public readonly uint StrokeMode; + public readonly uint StrokeHalfWidth; + public readonly uint StrokeLineCap; + public readonly uint StrokeLineJoin; + public readonly uint StrokeMiterLimit; + public readonly uint StrokePad0; public PreparedCompositeParameters( int destinationX, @@ -1769,7 +1898,12 @@ public PreparedCompositeParameters( float blendPercentage, Vector4 solidColor, uint rasterizationMode, - float antialiasThreshold) + float antialiasThreshold, + uint strokeMode = 0, + float strokeHalfWidth = 0f, + uint strokeLineCap = 0, + uint strokeLineJoin = 0, + float strokeMiterLimit = 0f) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -1797,6 +1931,12 @@ public PreparedCompositeParameters( this.SolidA = FloatToUInt32Bits(solidColor.W); this.RasterizationMode = rasterizationMode; this.AntialiasThreshold = FloatToUInt32Bits(antialiasThreshold); + this.StrokeMode = strokeMode; + this.StrokeHalfWidth = FloatToUInt32Bits(strokeHalfWidth); + this.StrokeLineCap = strokeLineCap; + this.StrokeLineJoin = strokeLineJoin; + this.StrokeMiterLimit = FloatToUInt32Bits(strokeMiterLimit); + this.StrokePad0 = 0; } } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs index 9481872d2..1dc119c31 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionBatch.cs @@ -18,7 +18,7 @@ public sealed class CompositionBatch /// Optional destination-local bounds touched by this batch. public CompositionBatch( in CompositionCoverageDefinition definition, - IReadOnlyList commands, + List commands, int flushId = 0, bool isFinalBatchInFlush = true, Rectangle? compositionBounds = null) @@ -38,7 +38,7 @@ public CompositionBatch( /// /// Gets normalized composition commands in original draw order. /// - public IReadOnlyList Commands { get; } + public List Commands { get; } /// /// Gets the batcher flush identifier shared by all batches emitted from one canvas flush call. diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index 4db3a8cd2..dbd64044d 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -20,6 +20,9 @@ public readonly struct CompositionCommand /// Graphics options used for composition. /// Rasterizer options used to generate coverage. /// Absolute destination offset where coverage is composited. + /// Optional stroke options for backend-side stroke expansion. + /// Stroke width in pixels when is present. + /// Optional dash pattern when is present. private CompositionCommand( int definitionKey, IPath path, @@ -27,7 +30,10 @@ private CompositionCommand( Rectangle brushBounds, GraphicsOptions graphicsOptions, in RasterizerOptions rasterizerOptions, - Point destinationOffset) + Point destinationOffset, + StrokeOptions? strokeOptions, + float strokeWidth, + ReadOnlyMemory strokePattern) { this.DefinitionKey = definitionKey; this.Path = path; @@ -36,6 +42,9 @@ private CompositionCommand( this.GraphicsOptions = graphicsOptions; this.RasterizerOptions = rasterizerOptions; this.DestinationOffset = destinationOffset; + this.StrokeOptions = strokeOptions; + this.StrokeWidth = strokeWidth; + this.StrokePattern = strokePattern; } /// @@ -74,7 +83,22 @@ private CompositionCommand( public Point DestinationOffset { get; } /// - /// Creates a composition command and computes a stable definition key from path geometry and rasterizer options. + /// Gets the stroke options when this command represents a stroke operation. + /// + public StrokeOptions? StrokeOptions { get; } + + /// + /// Gets the stroke width in pixels. + /// + public float StrokeWidth { get; } + + /// + /// Gets the optional dash pattern. + /// + public ReadOnlyMemory StrokePattern { get; } + + /// + /// Creates a fill composition command. /// /// Path to rasterize in target-local coordinates. /// Brush used during composition. @@ -92,17 +116,47 @@ public static CompositionCommand Create( Dictionary? definitionKeyCache = null) { int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions, definitionKeyCache); - RectangleF bounds = path.Bounds; - Rectangle localBrushBounds = Rectangle.FromLTRB( - (int)MathF.Floor(bounds.Left), - (int)MathF.Floor(bounds.Top), - (int)MathF.Ceiling(bounds.Right), - (int)MathF.Ceiling(bounds.Bottom)); - Rectangle brushBounds = new( - localBrushBounds.X + destinationOffset.X, - localBrushBounds.Y + destinationOffset.Y, - localBrushBounds.Width, - localBrushBounds.Height); + Rectangle brushBounds = ComputeBrushBounds(path, destinationOffset); + + return new( + definitionKey, + path, + brush, + brushBounds, + graphicsOptions, + in rasterizerOptions, + destinationOffset, + null, + 0f, + default); + } + + /// + /// Creates a stroke composition command where the backend is responsible for stroke expansion. + /// + /// The original centerline path in target-local coordinates. + /// Brush used during composition. + /// Graphics options used for composition. + /// Rasterizer options with interest inflated for stroke bounds. + /// Stroke geometry options. + /// Stroke width in pixels. + /// Optional dash pattern. Each element is a multiple of . + /// Absolute destination offset where coverage is composited. + /// Optional scoped cache to avoid repeated path flattening for the same reference. + /// The normalized stroke composition command. + public static CompositionCommand CreateStroke( + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + StrokeOptions strokeOptions, + float strokeWidth, + ReadOnlyMemory strokePattern = default, + Point destinationOffset = default, + Dictionary? definitionKeyCache = null) + { + int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions, definitionKeyCache); + Rectangle brushBounds = ComputeBrushBounds(rasterizerOptions.Interest, destinationOffset); return new( definitionKey, @@ -111,7 +165,10 @@ public static CompositionCommand Create( brushBounds, graphicsOptions, in rasterizerOptions, - destinationOffset); + destinationOffset, + strokeOptions, + strokeWidth, + strokePattern); } /// @@ -134,4 +191,26 @@ public static int ComputeCoverageDefinitionKey( (int)rasterizerOptions.SamplingOrigin); return HashCode.Combine(pathIdentity, rasterState); } + + private static Rectangle ComputeBrushBounds(IPath path, Point destinationOffset) + { + RectangleF bounds = path.Bounds; + Rectangle localBrushBounds = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + return new( + localBrushBounds.X + destinationOffset.X, + localBrushBounds.Y + destinationOffset.Y, + localBrushBounds.Width, + localBrushBounds.Height); + } + + private static Rectangle ComputeBrushBounds(Rectangle interest, Point destinationOffset) + => new( + interest.X + destinationOffset.X, + interest.Y + destinationOffset.Y, + interest.Width, + interest.Height); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs index 53d3f8d2f..882fa1d39 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -14,11 +14,42 @@ public readonly struct CompositionCoverageDefinition /// The stable key for this coverage definition. /// The path used to generate coverage. /// The rasterizer options used to generate coverage. - public CompositionCoverageDefinition(int definitionKey, IPath path, in RasterizerOptions rasterizerOptions) + /// The absolute destination offset where coverage is composited. + public CompositionCoverageDefinition( + int definitionKey, + IPath path, + in RasterizerOptions rasterizerOptions, + Point destinationOffset = default) + : this(definitionKey, path, in rasterizerOptions, destinationOffset, null, 0f, default) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The stable key for this coverage definition. + /// The path used to generate coverage. + /// The rasterizer options used to generate coverage. + /// The absolute destination offset where coverage is composited. + /// Optional stroke options. When present the path is the original centerline and the backend is responsible for stroke expansion. + /// The stroke width in pixels. Only meaningful when is not . + /// Optional dash pattern. Each element is a multiple of . + public CompositionCoverageDefinition( + int definitionKey, + IPath path, + in RasterizerOptions rasterizerOptions, + Point destinationOffset, + StrokeOptions? strokeOptions, + float strokeWidth, + ReadOnlyMemory strokePattern) { this.DefinitionKey = definitionKey; this.Path = path; this.RasterizerOptions = rasterizerOptions; + this.DestinationOffset = destinationOffset; + this.StrokeOptions = strokeOptions; + this.StrokeWidth = strokeWidth; + this.StrokePattern = strokePattern; } /// @@ -35,4 +66,33 @@ public CompositionCoverageDefinition(int definitionKey, IPath path, in Rasterize /// Gets the rasterizer options used to generate coverage. /// public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute destination offset where coverage is composited. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the stroke options when this definition represents a stroke operation. + /// + /// + /// When not , is the original centerline and the backend + /// is responsible for stroke expansion or SDF evaluation. + /// + public StrokeOptions? StrokeOptions { get; } + + /// + /// Gets the stroke width in pixels. + /// + public float StrokeWidth { get; } + + /// + /// Gets the optional dash pattern. Each element is a multiple of . + /// + public ReadOnlyMemory StrokePattern { get; } + + /// + /// Gets a value indicating whether this definition represents a stroke operation. + /// + public bool IsStroke => this.StrokeOptions is not null; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs index 36e9cee3c..35fcffa31 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScenePlanner.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -52,7 +53,11 @@ public static List CreatePreparedBatches( new( definitionKey, definitionCommand.Path, - definitionCommand.RasterizerOptions); + definitionCommand.RasterizerOptions, + definitionCommand.DestinationOffset, + definitionCommand.StrokeOptions, + definitionCommand.StrokeWidth, + definitionCommand.StrokePattern); batches.Add(new CompositionBatch(definition, preparedCommands)); } @@ -109,6 +114,63 @@ private static int EstimatePreparedCommandCapacity(int remainingCount) return 64; } + /// + /// Re-prepares batch commands after stroke expansion so destination regions + /// and source offsets match the actual outline interest. + /// + /// The prepared commands to update in place. + /// Target frame bounds in absolute coordinates. + /// The actual interest rect computed from the expanded outline. + public static void ReprepareBatchCommands( + List commands, + Rectangle targetBounds, + Rectangle interest) + { + Span span = CollectionsMarshal.AsSpan(commands); + int writeIndex = 0; + for (int i = 0; i < span.Length; i++) + { + ref PreparedCompositionCommand cmd = ref span[i]; + + Rectangle commandDestination = new( + cmd.DestinationOffset.X + interest.X, + cmd.DestinationOffset.Y + interest.Y, + interest.Width, + interest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, commandDestination); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + continue; + } + + Rectangle destinationLocalRegion = new( + clippedDestination.X - targetBounds.X, + clippedDestination.Y - targetBounds.Y, + clippedDestination.Width, + clippedDestination.Height); + + Point sourceOffset = new( + clippedDestination.X - commandDestination.X, + clippedDestination.Y - commandDestination.Y); + + cmd.DestinationRegion = destinationLocalRegion; + cmd.SourceOffset = sourceOffset; + + if (writeIndex != i) + { + span[writeIndex] = span[i]; + } + + writeIndex++; + } + + if (writeIndex < commands.Count) + { + commands.RemoveRange(writeIndex, commands.Count - writeIndex); + } + } + /// /// Clips one scene command to target bounds and computes coverage source offset mapping. /// @@ -150,7 +212,8 @@ public static bool TryPrepareCommand( sourceOffset, command.Brush, command.BrushBounds, - command.GraphicsOptions); + command.GraphicsOptions, + command.DestinationOffset); return true; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs b/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs deleted file mode 100644 index c5c3315a8..000000000 --- a/src/ImageSharp.Drawing/Processing/Backends/CoveragePreparationMode.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Preferred coverage preparation mode for a drawing operation. -/// -internal enum CoveragePreparationMode -{ - /// - /// Backend chooses its default coverage preparation path. - /// - Default = 0, - - /// - /// Backend should use fallback coverage preparation. - /// - Fallback = 1 -} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 474b86047..80f2a9937 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -154,8 +154,42 @@ private void FlushPreparedBatch( } CompositionCoverageDefinition definition = compositionBatch.Definition; + + // When the definition carries stroke metadata, expand the centerline + // path into a filled outline before rasterization. + IPath rasterPath = definition.Path; + RasterizerOptions rasterizerOptions = definition.RasterizerOptions; + + if (definition.IsStroke) + { + rasterPath = definition.StrokePattern.Length > 0 + ? rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokePattern.Span, definition.StrokeOptions!) + : rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokeOptions!); + + // Compute the exact interest from the actual stroke outline bounds + // so band boundaries and coverage values match the old canvas-side path. + RectangleF outlineBounds = rasterPath.Bounds; + outlineBounds = new RectangleF(outlineBounds.X + 0.5F, outlineBounds.Y + 0.5F, outlineBounds.Width, outlineBounds.Height); + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(outlineBounds.Left), + (int)MathF.Floor(outlineBounds.Top), + (int)MathF.Ceiling(outlineBounds.Right), + (int)MathF.Ceiling(outlineBounds.Bottom)); + + rasterizerOptions = new RasterizerOptions( + interest, + rasterizerOptions.IntersectionRule, + rasterizerOptions.RasterizationMode, + rasterizerOptions.SamplingOrigin, + rasterizerOptions.AntialiasThreshold); + + // Re-prepare commands with the actual outline interest so destination + // regions and source offsets are aligned with the rasterizer. + CompositionScenePlanner.ReprepareBatchCommands(compositionBatch.Commands, target.Bounds, interest); + } + Rectangle destinationBounds = destinationFrame.Rectangle; - IReadOnlyList commands = compositionBatch.Commands; + List commands = compositionBatch.Commands; int commandCount = commands.Count; BrushApplicator[] applicators = new BrushApplicator[commandCount]; try @@ -177,11 +211,11 @@ private void FlushPreparedBatch( commands, applicators, destinationBounds, - definition.RasterizerOptions.Interest.Top); + rasterizerOptions.Interest.Top); DefaultRasterizer.RasterizeRows( - definition.Path, - definition.RasterizerOptions, + rasterPath, + rasterizerOptions, configuration.MemoryAllocator, operation.InvokeCoverageRow, ref reusableScratch); @@ -198,13 +232,13 @@ private void FlushPreparedBatch( private readonly struct RowOperation where TPixel : unmanaged, IPixel { - private readonly IReadOnlyList commands; + private readonly List commands; private readonly BrushApplicator[] applicators; private readonly Rectangle destinationBounds; private readonly int coverageTop; public RowOperation( - IReadOnlyList commands, + List commands, BrushApplicator[] applicators, Rectangle destinationBounds, int coverageTop) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs b/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs deleted file mode 100644 index bff812445..000000000 --- a/src/ImageSharp.Drawing/Processing/Backends/DrawingCoverageHandle.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Opaque handle to backend-prepared coverage data. -/// -internal readonly struct DrawingCoverageHandle : IEquatable -{ - /// - /// Initializes a new instance of the struct. - /// - /// The backend-specific handle id. - public DrawingCoverageHandle(int value) => this.Value = value; - - /// - /// Gets the raw handle id. - /// - public int Value { get; } - - /// - /// Gets a value indicating whether this handle references prepared coverage. - /// - public bool IsValid => this.Value > 0; - - /// - /// Equality operator. - /// - /// Left value. - /// Right value. - /// if equal. - public static bool operator ==(DrawingCoverageHandle left, DrawingCoverageHandle right) => left.Equals(right); - - /// - /// Inequality operator. - /// - /// Left value. - /// Right value. - /// if not equal. - public static bool operator !=(DrawingCoverageHandle left, DrawingCoverageHandle right) => !(left == right); - - /// - public bool Equals(DrawingCoverageHandle other) => this.Value == other.Value; - - /// - public override bool Equals(object? obj) => obj is DrawingCoverageHandle other && this.Equals(other); - - /// - public override int GetHashCode() => this.Value; -} diff --git a/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs index 4fe2864e6..366782163 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One normalized composition command that applies a brush to the active coverage map. /// -public readonly struct PreparedCompositionCommand +public struct PreparedCompositionCommand { /// /// Initializes a new instance of the struct. @@ -16,29 +16,32 @@ public readonly struct PreparedCompositionCommand /// The brush used during composition. /// Brush bounds used for applicator creation. /// Graphics options used during composition. + /// The absolute destination offset from the original composition command. public PreparedCompositionCommand( Rectangle destinationRegion, Point sourceOffset, Brush brush, Rectangle brushBounds, - GraphicsOptions graphicsOptions) + GraphicsOptions graphicsOptions, + Point destinationOffset) { this.DestinationRegion = destinationRegion; this.SourceOffset = sourceOffset; this.Brush = brush; this.BrushBounds = brushBounds; this.GraphicsOptions = graphicsOptions; + this.DestinationOffset = destinationOffset; } /// - /// Gets the destination region in target-local coordinates. + /// Gets or sets the destination region in target-local coordinates. /// - public Rectangle DestinationRegion { get; } + public Rectangle DestinationRegion { get; set; } /// - /// Gets the source offset into the pre-rasterized coverage map. + /// Gets or sets the source offset into the pre-rasterized coverage map. /// - public Point SourceOffset { get; } + public Point SourceOffset { get; set; } /// /// Gets the brush used during composition. @@ -54,4 +57,9 @@ public PreparedCompositionCommand( /// Gets graphics options used during composition. /// public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the absolute destination offset from the original composition command. + /// + public Point DestinationOffset { get; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index f92462b2f..5dd97706e 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -467,8 +467,6 @@ public void Draw(Pen pen, IPath path) ? path : path.Transform(effectiveOptions.Transform); - IPath outline = pen.GeneratePath(transformedPath); - // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) { @@ -477,9 +475,23 @@ public void Draw(Pen pen, IPath path) effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); } - outline = ApplyClipPaths(outline, effectiveOptions.ShapeOptions, state.ClipPaths); + // When clip paths are active we must expand the stroke here so the clip + // boolean operation can be applied to the expanded outline geometry. + if (state.ClipPaths.Count > 0) + { + IPath outline = pen.GeneratePath(transformedPath); + outline = ApplyClipPaths(outline, effectiveOptions.ShapeOptions, state.ClipPaths); + this.PrepareCompositionCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); + return; + } - this.PrepareCompositionCore(outline, pen.StrokeFill, effectiveOptions, RasterizerSamplingOrigin.PixelCenter); + this.PrepareStrokeCompositionCore( + transformedPath, + pen.StrokeFill, + pen.StrokeWidth, + pen.StrokeOptions, + pen.StrokePattern, + effectiveOptions); } /// @@ -857,6 +869,64 @@ private void PrepareCompositionCore( this.targetFrame.Bounds.Location)); } + /// + /// Prepares a stroke composition command with the original centerline path and enqueues it. + /// The backend is responsible for stroke expansion or SDF evaluation. + /// + /// Original centerline path in target-local coordinates. + /// Brush used for shading. + /// Stroke width in pixels. + /// Stroke geometry options. + /// Optional dash pattern. + /// Effective drawing options. + private void PrepareStrokeCompositionCore( + IPath path, + Brush brush, + float strokeWidth, + StrokeOptions strokeOptions, + ReadOnlyMemory strokePattern, + DrawingOptions options) + { + GraphicsOptions graphicsOptions = options.GraphicsOptions; + ShapeOptions shapeOptions = options.ShapeOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + + // Inflate path bounds by the maximum possible stroke extent. + // The miter limit caps the tip extension; the base half-width is always present. + float halfWidth = strokeWidth / 2f; + float maxExtent = halfWidth * (float)Math.Max(strokeOptions.MiterLimit, 1D); + RectangleF bounds = path.Bounds; + bounds = new RectangleF( + bounds.X - maxExtent + 0.5F, + bounds.Y - maxExtent + 0.5F, + bounds.Width + (maxExtent * 2f), + bounds.Height + (maxExtent * 2f)); + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + shapeOptions.IntersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelCenter, + graphicsOptions.AntialiasThreshold); + + this.batcher.AddComposition( + CompositionCommand.CreateStroke( + path, + brush, + graphicsOptions, + rasterizerOptions, + strokeOptions, + strokeWidth, + strokePattern, + this.targetFrame.Bounds.Location)); + } + /// /// Converts rendered text operations to composition commands and submits them to the batcher. /// @@ -1028,61 +1098,83 @@ private CompositionCommand CreateCompositionCommand( operation.PixelAlphaCompositionMode, operation.PixelColorBlendingMode); - IPath compositionPath; - RasterizerSamplingOrigin samplingOrigin; - IntersectionRule intersectionRule = operation.IntersectionRule; + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + Point destinationOffset = new( + this.targetFrame.Bounds.X + operation.RenderLocation.X, + this.targetFrame.Bounds.Y + operation.RenderLocation.Y); + if (operation.Kind == DrawingOperationKind.Draw) { Pen pen = operation.Pen!; - compositionPath = pen.GeneratePath(operation.Path); - samplingOrigin = RasterizerSamplingOrigin.PixelCenter; + IPath path = operation.Path; // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. - if (intersectionRule != IntersectionRule.NonZero) - { - intersectionRule = IntersectionRule.NonZero; - } + IntersectionRule intersectionRule = operation.IntersectionRule != IntersectionRule.NonZero + ? IntersectionRule.NonZero + : operation.IntersectionRule; + + float halfWidth = pen.StrokeWidth / 2f; + float maxExtent = halfWidth * (float)Math.Max(pen.StrokeOptions.MiterLimit, 1D); + RectangleF bounds = path.Bounds; + bounds = new RectangleF( + bounds.X - maxExtent + 0.5F, + bounds.Y - maxExtent + 0.5F, + bounds.Width + (maxExtent * 2f), + bounds.Height + (maxExtent * 2f)); + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + intersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelCenter, + graphicsOptions.AntialiasThreshold); + + return CompositionCommand.CreateStroke( + path, + compositeBrush, + graphicsOptions, + rasterizerOptions, + pen.StrokeOptions, + pen.StrokeWidth, + pen.StrokePattern, + destinationOffset, + definitionKeyCache); } else { - compositionPath = operation.Path; - samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; - } - - RectangleF bounds = compositionPath.Bounds; - if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) - { - bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + IPath compositionPath = operation.Path; + RectangleF bounds = compositionPath.Bounds; + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + operation.IntersectionRule, + rasterizationMode, + RasterizerSamplingOrigin.PixelBoundary, + graphicsOptions.AntialiasThreshold); + + return CompositionCommand.Create( + compositionPath, + compositeBrush, + graphicsOptions, + rasterizerOptions, + destinationOffset, + definitionKeyCache); } - - Rectangle interest = Rectangle.FromLTRB( - (int)MathF.Floor(bounds.Left), - (int)MathF.Floor(bounds.Top), - (int)MathF.Ceiling(bounds.Right), - (int)MathF.Ceiling(bounds.Bottom)); - - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - RasterizerOptions rasterizerOptions = new( - interest, - intersectionRule, - rasterizationMode, - samplingOrigin, - graphicsOptions.AntialiasThreshold); - - Point destinationOffset = new( - this.targetFrame.Bounds.X + operation.RenderLocation.X, - this.targetFrame.Bounds.Y + operation.RenderLocation.Y); - - return CompositionCommand.Create( - compositionPath, - compositeBrush, - graphicsOptions, - rasterizerOptions, - destinationOffset, - definitionKeyCache); } /// diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 8f85739e5..4f7b7d811 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -3,7 +3,6 @@ using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Drawing.Processing; diff --git a/src/ImageSharp.Drawing/Processing/PatternPen.cs b/src/ImageSharp.Drawing/Processing/PatternPen.cs index f6da8ee04..75b7e32bb 100644 --- a/src/ImageSharp.Drawing/Processing/PatternPen.cs +++ b/src/ImageSharp.Drawing/Processing/PatternPen.cs @@ -75,5 +75,5 @@ public override bool Equals(Pen? other) /// public override IPath GeneratePath(IPath path, float strokeWidth) - => path.GenerateOutline(strokeWidth, this.StrokePattern, this.StrokeOptions); + => path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokeOptions); } diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index e3fbd3094..0ad0b0b39 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -80,7 +80,7 @@ protected Pen(PenOptions options) public float StrokeWidth { get; } /// - public ReadOnlySpan StrokePattern => this.pattern; + public ReadOnlyMemory StrokePattern => this.pattern; /// public StrokeOptions StrokeOptions { get; } @@ -107,7 +107,7 @@ public virtual bool Equals(Pen? other) && this.StrokeWidth == other.StrokeWidth && this.StrokeFill.Equals(other.StrokeFill) && this.StrokeOptions.Equals(other.StrokeOptions) - && this.StrokePattern.SequenceEqual(other.StrokePattern); + && this.StrokePattern.Span.SequenceEqual(other.StrokePattern.Span); /// public override bool Equals(object? obj) => this.Equals(obj as Pen); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs deleted file mode 100644 index 4abe103e3..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackend.cs +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SkiaSharp; - -namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; - -internal sealed class SkiaCoverageDrawingBackend : IDrawingBackend, IDisposable -{ - private readonly ConcurrentDictionary preparedCoverage = new(); - private int nextCoverageHandleId; - private bool isDisposed; - - public int PrepareCoverageCallCount { get; private set; } - - public int CompositeCoverageCallCount { get; private set; } - - public int ReleaseCoverageCallCount { get; private set; } - - public int LiveCoverageCount => this.preparedCoverage.Count; - - public void FlushCompositions( - Configuration configuration, - ICanvasFrame target, - CompositionScene compositionScene) - where TPixel : unmanaged, IPixel - { - if (compositionScene.Commands.Count == 0) - { - return; - } - - List preparedBatches = CompositionScenePlanner.CreatePreparedBatches( - compositionScene.Commands, - target.Bounds); - for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) - { - CompositionBatch compositionBatch = preparedBatches[batchIndex]; - if (compositionBatch.Commands.Count == 0) - { - continue; - } - - CompositionCoverageDefinition definition = compositionBatch.Definition; - DrawingCoverageHandle coverageHandle = this.PrepareCoverage( - definition.Path, - definition.RasterizerOptions, - configuration.MemoryAllocator, - CoveragePreparationMode.Default); - try - { - IReadOnlyList commands = compositionBatch.Commands; - for (int i = 0; i < commands.Count; i++) - { - PreparedCompositionCommand composition = commands[i]; - ICanvasFrame commandTarget = new CanvasRegionFrame(target, composition.DestinationRegion); - - this.CompositeCoverage( - configuration, - commandTarget, - coverageHandle, - composition.SourceOffset, - composition.Brush, - composition.GraphicsOptions, - composition.BrushBounds); - } - } - finally - { - this.ReleaseCoverage(coverageHandle); - } - } - } - - public bool TryReadRegion( - Configuration configuration, - ICanvasFrame target, - Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) - where TPixel : unmanaged, IPixel - { - image = null; - return false; - } - - public DrawingCoverageHandle PrepareCoverage( - IPath path, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - CoveragePreparationMode preparationMode) - { - ArgumentNullException.ThrowIfNull(path); - - ArgumentNullException.ThrowIfNull(allocator); - _ = preparationMode; - - this.PrepareCoverageCallCount++; - - Size size = rasterizerOptions.Interest.Size; - if (size.Width <= 0 || size.Height <= 0) - { - return default; - } - - SKImageInfo imageInfo = new(size.Width, size.Height, SKColorType.Alpha8, SKAlphaType.Unpremul); - SKBitmap bitmap = new(imageInfo); - using SKCanvas canvas = new(bitmap); - canvas.Clear(SKColors.Transparent); - - if (rasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter) - { - canvas.Translate(0.5F, 0.5F); - } - - using SKPath skPath = CreateSkPath(path, rasterizerOptions.Interest.Location, rasterizerOptions.IntersectionRule); - using SKPaint paint = new() - { - Color = SKColors.White, - Style = SKPaintStyle.Fill, - IsAntialias = rasterizerOptions.RasterizationMode == RasterizationMode.Antialiased - }; - - canvas.DrawPath(skPath, paint); - - int handleId = Interlocked.Increment(ref this.nextCoverageHandleId); - if (!this.preparedCoverage.TryAdd(handleId, bitmap)) - { - bitmap.Dispose(); - throw new InvalidOperationException("Failed to cache prepared coverage."); - } - - return new DrawingCoverageHandle(handleId); - } - - public void CompositeCoverage( - Configuration configuration, - ICanvasFrame target, - DrawingCoverageHandle coverageHandle, - Point sourceOffset, - Brush brush, - in GraphicsOptions graphicsOptions, - Rectangle brushBounds) - where TPixel : unmanaged, IPixel - { - ArgumentNullException.ThrowIfNull(configuration); - - ArgumentNullException.ThrowIfNull(brush); - - this.CompositeCoverageCallCount++; - - if (!coverageHandle.IsValid) - { - return; - } - - if (!this.preparedCoverage.TryGetValue(coverageHandle.Value, out SKBitmap bitmap)) - { - throw new InvalidOperationException($"Prepared coverage handle '{coverageHandle.Value}' is not valid."); - } - - if (!target.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) - { - throw new NotSupportedException( - $"{nameof(SkiaCoverageDrawingBackend)} requires CPU-accessible frame targets for {nameof(this.CompositeCoverage)}."); - } - - if (bitmap.ColorType != SKColorType.Alpha8) - { - throw new InvalidOperationException($"Prepared coverage '{coverageHandle.Value}' is not Alpha8."); - } - - if ((uint)sourceOffset.X >= (uint)bitmap.Width || (uint)sourceOffset.Y >= (uint)bitmap.Height) - { - return; - } - - int compositeWidth = Math.Min(destinationRegion.Width, bitmap.Width - sourceOffset.X); - int compositeHeight = Math.Min(destinationRegion.Height, bitmap.Height - sourceOffset.Y); - if (compositeWidth <= 0 || compositeHeight <= 0) - { - return; - } - - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - graphicsOptions, - destinationRegion, - brushBounds); - - ReadOnlySpan source = bitmap.GetPixelSpan(); - int rowBytes = bitmap.RowBytes; - int absoluteX = destinationRegion.Rectangle.X; - int absoluteY = destinationRegion.Rectangle.Y; - - float[] rented = ArrayPool.Shared.Rent(compositeWidth); - try - { - Span coverage = rented.AsSpan(0, compositeWidth); - for (int row = 0; row < compositeHeight; row++) - { - int srcRow = (sourceOffset.Y + row) * rowBytes; - int srcOffset = srcRow + sourceOffset.X; - for (int x = 0; x < compositeWidth; x++) - { - coverage[x] = source[srcOffset + x] / 255F; - } - - applicator.Apply(coverage, absoluteX, absoluteY + row); - } - } - finally - { - ArrayPool.Shared.Return(rented); - } - } - - public void ReleaseCoverage(DrawingCoverageHandle coverageHandle) - { - this.ReleaseCoverageCallCount++; - - if (!coverageHandle.IsValid) - { - return; - } - - if (this.preparedCoverage.TryRemove(coverageHandle.Value, out SKBitmap bitmap)) - { - bitmap.Dispose(); - } - } - - public void Dispose() - { - if (this.isDisposed) - { - return; - } - - foreach (KeyValuePair kv in this.preparedCoverage) - { - kv.Value.Dispose(); - } - - this.preparedCoverage.Clear(); - this.isDisposed = true; - } - - private static SKPath CreateSkPath(IPath path, Point interestLocation, IntersectionRule intersectionRule) - { - SKPath skPath = new() - { - FillType = intersectionRule == IntersectionRule.EvenOdd - ? SKPathFillType.EvenOdd - : SKPathFillType.Winding - }; - - float offsetX = -interestLocation.X; - float offsetY = -interestLocation.Y; - - foreach (ISimplePath simplePath in path.Flatten()) - { - ReadOnlySpan points = simplePath.Points.Span; - if (points.Length == 0) - { - continue; - } - - SKPoint[] skPoints = new SKPoint[points.Length]; - for (int i = 0; i < points.Length; i++) - { - skPoints[i] = new SKPoint(points[i].X + offsetX, points[i].Y + offsetY); - } - - skPath.AddPoly(skPoints, true); - } - - return skPath; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs deleted file mode 100644 index 59010c027..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/SkiaCoverageDrawingBackendTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; - -[GroupOutput("Drawing")] -public class SkiaCoverageDrawingBackendTests -{ - [Theory] - [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] - public void DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) - { - Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); - RichTextOptions textOptions = new(font) - { - Origin = new PointF(18, 28) - }; - - DrawingOptions drawingOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true } - }; - - string text = "Sphinx of black quartz, judge my vow\n0123456789"; - Brush brush = Brushes.Solid(Color.Black); - Pen pen = Pens.Solid(Color.OrangeRed, 2F); - - using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen))); - defaultImage.DebugSave( - provider, - "DefaultBackend_DrawText", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - using Image skiaBackendImage = provider.GetImage(); - using SkiaCoverageDrawingBackend backend = new(); - skiaBackendImage.Configuration.SetDrawingBackend(backend); - skiaBackendImage.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen))); - - skiaBackendImage.DebugSave( - provider, - "SkiaBackend_DrawText", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - Assert.True(backend.PrepareCoverageCallCount > 0); - Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); - - ImageComparer comparer = ImageComparer.TolerantPercentage(4F); - comparer.VerifySimilarity(defaultImage, skiaBackendImage); - } - - [Theory] - [WithSolidFilledImages(420, 220, "White", PixelTypes.Rgba32)] - public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) - { - Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); - RichTextOptions textOptions = new(font) - { - Origin = new PointF(8, 8), - WrappingLength = 400 - }; - - DrawingOptions drawingOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true } - }; - - string text = new('A', 200); - Brush brush = Brushes.Solid(Color.Black); - - using Image image = provider.GetImage(); - using SkiaCoverageDrawingBackend backend = new(); - image.Configuration.SetDrawingBackend(backend); - - image.Mutate(ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText(textOptions, text, brush, pen: null))); - - image.DebugSave( - provider, - "SkiaBackend_RepeatedGlyphs", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - Assert.InRange(backend.PrepareCoverageCallCount, 1, 20); - Assert.True(backend.CompositeCoverageCallCount >= backend.PrepareCoverageCallCount); - Assert.Equal(backend.PrepareCoverageCallCount, backend.ReleaseCoverageCallCount); - Assert.Equal(0, backend.LiveCoverageCount); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 39bf68f94..cb338de92 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -434,11 +434,14 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); + + // Stroking difference are minor subpixel differences but accumulate more than typical rasterization differences, + // so use a higher threshold here and below. + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.0292F); Rectangle textRegion = Rectangle.Intersect( new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); - AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.01F); + AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.0376F); } [Theory] @@ -566,7 +569,9 @@ void DrawAction(DrawingCanvas canvas) AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); + + // Differences are visually allowable so use a higher threshold here. + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.0516F); } [Theory] diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 3ef419f0d..75a88d3c9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; +using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Memory; @@ -37,6 +38,94 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() Assert.Same(brushB, backend.LastBatch.Commands[1].Brush); } + [Fact] + public void Flush_SamePathDifferentBrushes_Stroke_UsesSingleBatch() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(40, 40); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + IPath path = new RectangularPolygon(4, 6, 18, 12); + DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, region, options); + Pen penA = Pens.Solid(Color.Red, 2F); + Pen penB = Pens.Solid(Color.Blue, 2F); + + canvas.Draw(penA, path); + canvas.Draw(penB, path); + canvas.Flush(); + + Assert.Single(backend.Batches); + Assert.True(backend.LastBatch.Definition.IsStroke); + Assert.Equal(2, backend.LastBatch.Commands.Count); + } + + [Fact] + public void Flush_SamePathReusedMultipleTimes_BatchesCommands() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(100, 100); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + // Use the same path reference 10 times with different brushes. + IPath path = new RectangularPolygon(10, 10, 40, 40); + DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, region, options); + + for (int i = 0; i < 10; i++) + { + canvas.Fill(path, Brushes.Solid(Color.FromPixel(new Rgba32((byte)i, 0, 0, 255)))); + } + + canvas.Flush(); + + // All 10 commands share the same path reference → single batch. + Assert.Single(backend.Batches); + Assert.Equal(10, backend.Batches[0].Commands.Count); + } + + [Fact] + public void Flush_RepeatedGlyphs_ReusesCoverageDefinitions() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(420, 220); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + + using DrawingCanvas canvas = new(configuration, region, drawingOptions); + canvas.DrawText(textOptions, text, brush, pen: null); + canvas.Flush(); + + int totalCommands = backend.Batches.Sum(b => b.Commands.Count); + Assert.True(totalCommands > 0); + + // The glyph renderer caches paths within 1/8th pixel sub-pixel offset, + // so 200 identical glyphs reuse coverage definitions across sub-pixel variants. + Assert.True( + backend.Batches.Count < 200, + $"Expected coverage reuse but got {backend.Batches.Count} batches for 200 glyphs."); + } + private sealed class CapturingBackend : IDrawingBackend { public List Batches { get; } = []; @@ -53,7 +142,7 @@ private sealed class CapturingBackend : IDrawingBackend RasterizationMode.Aliased, RasterizerSamplingOrigin.PixelBoundary, 0.5f)), - Array.Empty()); + []); public void FlushCompositions( Configuration configuration, diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png index fab753ae5..7073fa8d2 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2a4cbc37c65d3d58ace60086f4eb80b0bf683313db280e820d394bef6f43e38 -size 13870 +oid sha256:90edf38f6d93d8c9142172a33b1e94948530984f250677f201e4af382ff091f1 +size 13865 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png index 27c560fea..ac71bf251 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23f3f42a3dbe57707ce5f488b1495a58b213577a9a06a45babb28b65abf1606b -size 7127 +oid sha256:8b9061fe07c30c4d8ad28dde127ef4153d0a522d3c3040184baafa7b7c8782c7 +size 3556 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png index 8635613c1..75ee17c04 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5823d96d7d2eaba23b23c716c0337000c98baa35b38aa67fd35cfe0a9f00dc94 -size 3831 +oid sha256:6cb8583ce253d500309296ac2bbee670a46aaa56162f5103305d22f02d1e90a1 +size 3449 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png index b370b2fe4..05629b290 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:260ec3704eeed9f6d005bfd3475fe5b812a253543bd9726b8e8c7e562b3b73c8 -size 13277 +oid sha256:d4d703eefd1e0c88bc6bd952382260179f513d2f9c65c9b0ef25943ae8d1e6b2 +size 11158 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png index dfb8cdd0b..96086b63c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2aa8c26eb90c0892eb8c8cbf4094ffac251aafa42929cda34f0183485c643f4 -size 6665 +oid sha256:698a8a68e934d84a5689d0eb2e58ef32c389c3c19149bcc668ea1cc061eb39df +size 4983 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png index 2e4f1c99b..7df5c6d23 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd22a2fb86561ee9871b2427333d5390ee943bf52270b5ab788cefa15c3cb07 -size 4192 +oid sha256:f89b5cf4014bd7ad886e10bd92cde5c00ef00ab13c78062f7fe39318e8bbbc45 +size 2255 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png index c6439fdc5..b0c830f95 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4edce89e09ede18430cff57ff74c21bccbac076c5d015d0ca77d039fc586fc62 -size 1747 +oid sha256:d4fa5eec491aaf3ece0c1d6a842c667e172c1697eac725f4ef2672742b9b695a +size 3815 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png index 1b997c824..6408b21ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a77a50279300c53b00dd01518c20b7ae08fcd8b1ecf567b2d6a6be4e6dbf28 -size 7723 +oid sha256:cf91c8aac027e1bbb9629eab3cd2f06db76e15281b6f7b93fbb6d2bfae78b151 +size 16016 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png index 4101cdaf3..c1c9b090d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09d7232b67122a42a196638c9a8064c6e8deccc771ff52cb3e8be10b1cb99639 -size 14978 +oid sha256:9cbd1507004337a94fd4121cd006c57d0ae52db55c34edf84d8caca94128a933 +size 16942 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png index 033d08f8c..5a228a7ce 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee87393ba69032cdf05002c9f7bdb9a74831b4c42e2afd38e8308e50fd23eaf8 -size 7361 +oid sha256:2c6da672f1b187d7296ad7443609582fc1eba9578d23d1005b71049e303a1452 +size 14856 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png index d1417fb8f..0d70e48eb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad8f0bba3425685db654e18781bfee25e587fb669b68d0b4e948c7000072ca2 -size 20130 +oid sha256:559421345d453431dad84bc84c1afd920dafadaa09cc421d009ee039feed99a9 +size 10038 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png index 7aa5c5e0e..474e3f1cd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4381267ab8e5c6d09487f79ee861ca4929eb91a7af5cfd3fd42a8664bac448ed -size 5349 +oid sha256:d498f83cc3cf2a2f47646f7909479e6cbba8ca2dba07e01e99d0023f02762e51 +size 2279 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png index 34e6d4e26..1e380775b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a263e68940d4a45c69b083461d92f71008cb9bd5d972f99a824ce1702ebb0676 -size 5780 +oid sha256:9030fe85b5d13168b4308b9bab1af7ddf6cab90a601e3d90ef5b0d8094379fda +size 2600 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png index b0bcecd96..ea9167458 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50ab386ba5cb492955b31ef1b60bfad24222e34cab065e9b5d8da7f9d18ef410 -size 5226 +oid sha256:7bf8e56e1e7357fd33c076d1b75788706f8cd8629c3a557471770a22b4aba95e +size 2266 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 73217aad8..3c560e659 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d15ab9435f0c2167443434422c420a6c26e7749f88c12751dd6f74cc4f013f7 +oid sha256:fb13ecdb980ccb0bc08df4e1628af9dd4a14368b17f717c1e571262836278b3a size 17378 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 08dc9ee18..8c9c2ecbf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:229b4930840c10211e554f63ebb8c5701830ea27eb298f65cd6ddcb99ee1c3fb +oid sha256:79319b83ce8c32bace057288383b441b8d2c9ae96c16e9ffc3d5e2a227d61b63 size 16823 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png index 8a9192058..3f6fa2cf9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9d8d6a0327d1d248476e52e6d5926757989471a9bd300a4b84af92efe58d1af -size 232019 +oid sha256:427f884c9c1dad119bd8478a353796b55646dbad64f23ce211bf2482cee31cda +size 115461 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 9bfbfd1e7..84d9e5390 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00b742a18ee40eaa558de7649ee939935459e7a47beb54f80a7c735f584a7afc -size 21475 +oid sha256:740c8f52dad954ac302f1c88155c1c41e0219fd4a93d9c7f5e00542260d7b8be +size 11166 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png index 4ba7d7ba5..5217fcd88 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfbd2e1028aed3bab74464d5d29b6c8b06c4181fe374368f0b587439362a073f -size 18146 +oid sha256:1ab1ec5f1606225bad6517c9f7ac54a48ce96b9cec86793e99a31e054a06fcbe +size 18147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png index 087e32589..7fb314d05 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36935a802152d9cb8b6b8f26af66ca7485331f0f9cf43eac5c98bc9f64b49c24 +oid sha256:e15dd4e0a7d46d3877c3848b19a61618fb7734041c7ccdde5dceb22e6408aa07 size 34735 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index 911950d6c..b58f0a478 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13e5d2a6cb238750401137ee17c9e3ce4f1067218e1466b2ddb03fd3b162ddb0 +oid sha256:50bcdda1794425a3096665c4e8763bd89310e79bc331d8d28512d6d5a3f55e3a size 4486 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index 5025bff97..7d6f87f8f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc551f322b933a876d450c9a1af0e8914f047ab5a471e7f7b4c228d2619e89c5 -size 41042 +oid sha256:9a962b83d330b3f08ce403d9b8b110173367d2a075515280a356779cd96a3065 +size 41039 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png index 40993557f..e517a34a8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f1418f50b3831f6aa838b70bdaf7765a5ddddb0a693c4e8c3537e06a0a70e98 +oid sha256:43d49cdfb9c602789452dba4b6c02b2b1c2c0517f10d19aed7ace0a9e9e96bd5 size 407764 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png index 613ac0e05..83661b2cc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d64a09bfa99e9c0e90b1c02c8bbaaa0fa4afa5a92d8988388d4ead6aa769dea -size 4822 +oid sha256:2899498199c9b9c5de959be83a6d8091d8f75319efb8964b0610900e978c1144 +size 9603 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png index 05cecb028..2265c505f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4267e18ce07a5b0dccef1cfb6e96968e4cd66939d3f411a2fa476035165794c -size 6517 +oid sha256:1d8d4ddb806acf510c308d2644b316006becefd9f07aed36b4bd8b8d850bfa41 +size 1816 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png index 05cecb028..2265c505f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4267e18ce07a5b0dccef1cfb6e96968e4cd66939d3f411a2fa476035165794c -size 6517 +oid sha256:1d8d4ddb806acf510c308d2644b316006becefd9f07aed36b4bd8b8d850bfa41 +size 1816 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png index e4dcbc49c..7024ebfef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0553e98786c9f025abe2acc8aa86fa6fed02909d6524b5ca847600a79709adb7 -size 6513 +oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 +size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png index e4dcbc49c..7024ebfef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0553e98786c9f025abe2acc8aa86fa6fed02909d6524b5ca847600a79709adb7 -size 6513 +oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 +size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png index ce3ccd62f..283285402 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:010f089d7793900d2afc224596bca74a90557b16aaefcc3f4453e51d277eb8db -size 36436 +oid sha256:fec9ff20f83a4f537e5a743094cc1edb263676fa173cf6790481ae76ecb81890 +size 34079 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png index ce3ccd62f..283285402 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:010f089d7793900d2afc224596bca74a90557b16aaefcc3f4453e51d277eb8db -size 36436 +oid sha256:fec9ff20f83a4f537e5a743094cc1edb263676fa173cf6790481ae76ecb81890 +size 34079 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png new file mode 100644 index 000000000..71fe4495e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 +size 523 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png new file mode 100644 index 000000000..71fe4495e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 +size 523 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png new file mode 100644 index 000000000..71fe4495e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 +size 523 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png index 64b7db546..1b1ed3e3b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98b346983c2204e1df96b17a68817c39d97f754d2250a5d293ae3715bc5c6893 -size 4153 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png index 64b7db546..1b1ed3e3b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98b346983c2204e1df96b17a68817c39d97f754d2250a5d293ae3715bc5c6893 -size 4153 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png index 9d2de9c91..0d51f838c 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ede5d07fadc0fd9584fb0816d30d7d44482e0df58dc357f8657b1e175cc6e2f -size 3779 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png index 9d2de9c91..0d51f838c 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ede5d07fadc0fd9584fb0816d30d7d44482e0df58dc357f8657b1e175cc6e2f -size 3779 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png index f2e9762e2..00a793ec2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb8b2d1ca50be5e745fa5e8a2a303fae80440b4b953da8b4cdb32d126527482a -size 3335 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png index f2e9762e2..00a793ec2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb8b2d1ca50be5e745fa5e8a2a303fae80440b4b953da8b4cdb32d126527482a -size 3335 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png index 4837b94a1..443c5e78e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b91be13b16fc40fa5253d3c0839cbe0a5532dc72e42bb85e2f3e8224d3201ef5 -size 4186 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png index 4837b94a1..443c5e78e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b91be13b16fc40fa5253d3c0839cbe0a5532dc72e42bb85e2f3e8224d3201ef5 -size 4186 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png index 1e1ded962..c561128ef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca773106b7ba2857ed764535e536dcb08b2be55deff78c5494be8bad4bb8f9a -size 2947 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png index 1e1ded962..c561128ef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca773106b7ba2857ed764535e536dcb08b2be55deff78c5494be8bad4bb8f9a -size 2947 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png index 3aed91e90..1ad01578b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0021289109332c93e7cb92733d57466c6f9aa3f9cdf24302876078a208476cc -size 3010 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png index 3aed91e90..1ad01578b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0021289109332c93e7cb92733d57466c6f9aa3f9cdf24302876078a208476cc -size 3010 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png index 2045f5391..55a946401 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74c5cf879133de1afbd22980222469fe169899108d8a823e63174120fc681dec -size 3295 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png index 2045f5391..55a946401 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74c5cf879133de1afbd22980222469fe169899108d8a823e63174120fc681dec -size 3295 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png index 218e72a90..e56076d85 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bef8c9b3fc1f857240c0d7a219e09604b14a14bed0c5344eae1a0670f651f1fd -size 12935 +oid sha256:89b70c1701bb62b2d879e595555bf5c19f973ff9ec15f2b1ffcf3029aa96fcda +size 12204 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png index 218e72a90..e56076d85 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bef8c9b3fc1f857240c0d7a219e09604b14a14bed0c5344eae1a0670f651f1fd -size 12935 +oid sha256:89b70c1701bb62b2d879e595555bf5c19f973ff9ec15f2b1ffcf3029aa96fcda +size 12204 From fa6b1db3ef7fb5d4b0b4609b65e7e8599bffd533 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 13:56:00 +1000 Subject: [PATCH 107/136] Add GPU stroke expand shader & refactor strokes --- .../Shaders/CompositeComputeShader.cs | 181 +---- .../Shaders/StrokeExpandComputeShader.cs | 311 ++++++++ ...WebGPUDrawingBackend.CoverageRasterizer.cs | 719 ++++++++++-------- .../WebGPUDrawingBackend.cs | 322 ++++++-- .../WebGPUFlushContext.cs | 5 +- .../ImageSharp.Drawing.Benchmarks/Program.cs | 2 +- .../Backends/WebGPUDrawingBackendTests.cs | 121 ++- ...t_DrawPath_Stroke_LineCap_Butt_Default.png | 3 + ...h_Stroke_LineCap_Butt_WebGPU_CPURegion.png | 3 + ...roke_LineCap_Butt_WebGPU_NativeSurface.png | 3 + ..._DrawPath_Stroke_LineCap_Round_Default.png | 3 + ..._Stroke_LineCap_Round_WebGPU_CPURegion.png | 3 + ...oke_LineCap_Round_WebGPU_NativeSurface.png | 3 + ...DrawPath_Stroke_LineCap_Square_Default.png | 3 + ...Stroke_LineCap_Square_WebGPU_CPURegion.png | 3 + ...ke_LineCap_Square_WebGPU_NativeSurface.png | 3 + ...DrawPath_Stroke_LineJoin_Bevel_Default.png | 3 + ...Stroke_LineJoin_Bevel_WebGPU_CPURegion.png | 3 + ...ke_LineJoin_Bevel_WebGPU_NativeSurface.png | 3 + ...th_Stroke_LineJoin_MiterRevert_Default.png | 3 + ..._LineJoin_MiterRevert_WebGPU_CPURegion.png | 3 + ...eJoin_MiterRevert_WebGPU_NativeSurface.png | 3 + ...ath_Stroke_LineJoin_MiterRound_Default.png | 3 + ...e_LineJoin_MiterRound_WebGPU_CPURegion.png | 3 + ...neJoin_MiterRound_WebGPU_NativeSurface.png | 3 + ...DrawPath_Stroke_LineJoin_Miter_Default.png | 3 + ...Stroke_LineJoin_Miter_WebGPU_CPURegion.png | 3 + ...ke_LineJoin_Miter_WebGPU_NativeSurface.png | 3 + ...DrawPath_Stroke_LineJoin_Round_Default.png | 3 + ...Stroke_LineJoin_Round_WebGPU_CPURegion.png | 3 + ...ke_LineJoin_Round_WebGPU_NativeSurface.png | 3 + ...utput_DrawPath_Stroke_WebGPU_CPURegion.png | 4 +- ...t_DrawPath_Stroke_WebGPU_NativeSurface.png | 4 +- ...aredCoverage_DrawText_WebGPU_CPURegion.png | 4 +- ...Coverage_DrawText_WebGPU_NativeSurface.png | 4 +- ...DefaultOutput_Process_WebGPU_CPURegion.png | 4 +- ...ultOutput_Process_WebGPU_NativeSurface.png | 4 +- 37 files changed, 1207 insertions(+), 550 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/StrokeExpandComputeShader.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 5ffec30be..e1697a52a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -25,6 +25,8 @@ struct Edge { x1: i32, y1: i32, flags: i32, + adj_x: i32, + adj_y: i32, } struct Params { @@ -54,12 +56,8 @@ struct Params { solid_a: u32, rasterization_mode: u32, antialias_threshold: u32, - stroke_mode: u32, - stroke_half_width: u32, - stroke_line_cap: u32, - stroke_line_join: u32, - stroke_miter_limit: u32, - stroke_pad0: u32, + pad0: u32, + pad1: u32, }; struct DispatchConfig { @@ -814,148 +812,6 @@ fn area_to_coverage(area_val: i32, fill_rule: u32, rasterization_mode: u32, anti return coverage; } - // ----------------------------------------------------------------------- - // Stroke distance-field helpers - // ----------------------------------------------------------------------- - - // Returns vec2(squared_distance, unclamped_t) from point p to line segment a→b. - // The returned distance uses clamped t (0..1), but the unclamped t is also returned - // so callers can detect endpoint proximity for cap handling. - fn dist_to_segment(p: vec2, a: vec2, b: vec2) -> vec2 { - let ab = b - a; - let ap = p - a; - let len_sq = dot(ab, ab); - var unclamped_t: f32; - if len_sq < 1e-10 { - unclamped_t = 0.0; - } else { - unclamped_t = dot(ap, ab) / len_sq; - } - let t = clamp(unclamped_t, 0.0, 1.0); - let closest = a + ab * t; - let d = p - closest; - return vec2(dot(d, d), unclamped_t); - } - - // Edge flags (matches C# GpuEdge.Flags bit layout). - const EDGE_OPEN_START: i32 = 1; // (x0,y0) is an open path start - const EDGE_OPEN_END: i32 = 2; // (x1,y1) is an open path end - - // LineCap enum values. - const CAP_BUTT: u32 = 0u; - const CAP_SQUARE: u32 = 1u; - const CAP_ROUND: u32 = 2u; - - // Computes stroke coverage for a pixel using distance-field evaluation. - // Iterates all edges in relevant bands, finds min distance to centerline, - // applies line cap rules using edge flags, and returns antialiased coverage. - // - // Line join handling: - // Miter/MiterRevert/MiterRound joins are handled by extending centerline - // segments past interior vertices on the CPU side. The distance field then - // naturally produces the correct miter coverage. - // Round joins are the natural distance-field behavior. - // Bevel join coverage is very close to round and accepted as-is. - fn stroke_coverage( - dest_x_i32: i32, - dest_y_i32: i32, - command: Params, - half_width: f32, - line_cap: u32, - line_join: u32, - miter_limit: f32, - tile_min_x: i32, - tile_min_y: i32, - ) -> f32 { - let px = f32(dest_x_i32 - command.edge_origin_x) + 0.5; - let py = f32(dest_y_i32 - command.edge_origin_y) + 0.5; - let point = vec2(px, py); - - // Determine band range for this pixel. Edges are stored per 16-row band. - // We must check all bands whose edges could be within half_width + 1 distance. - let expand = i32(ceil(half_width)) + 1; - let pixel_y = i32(py); - var first_band = max((pixel_y - expand) / 16, 0); - if (pixel_y - expand) < 0 && ((pixel_y - expand) % 16) != 0 { - first_band = max(first_band - 1, 0); - } - let last_band = min((pixel_y + expand) / 16, i32(command.csr_band_count) - 1); - if first_band > last_band || i32(command.csr_band_count) == 0 { - return 0.0; - } - - let sentinel = half_width * half_width + half_width * 2.0 + 1.0; - var min_dist_sq = sentinel; - let inv_fixed = 1.0 / f32(FIXED_ONE); - - for (var band = first_band; band <= last_band; band++) { - let b_start = band_offsets[command.csr_offsets_start + u32(band)]; - let b_end = band_offsets[command.csr_offsets_start + u32(band) + 1u]; - for (var ei = b_start; ei < b_end; ei++) { - let edge = edges[command.edge_start + ei]; - let a = vec2(f32(edge.x0) * inv_fixed, f32(edge.y0) * inv_fixed); - let b = vec2(f32(edge.x1) * inv_fixed, f32(edge.y1) * inv_fixed); - let flags = edge.flags; - let is_open_start = (flags & EDGE_OPEN_START) != 0; - let is_open_end = (flags & EDGE_OPEN_END) != 0; - - let result = dist_to_segment(point, a, b); - var d_sq = result.x; - let unclamped_t = result.y; - - // Cap handling at open path endpoints. - // Interior vertices (no open flags) use the natural clamped distance, - // which produces smooth coverage where adjacent segments meet. - if line_cap == CAP_BUTT { - // Butt cap: no coverage past the open endpoint. - // Exclude this edge if the pixel projects past an open end. - if (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end) { - d_sq = sentinel; - } - } else if line_cap == CAP_SQUARE { - // Square cap: extend the segment by half_width at open endpoints only. - let needs_ext = (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end); - if needs_ext { - let seg = b - a; - let seg_len = length(seg); - if seg_len > 1e-6 { - let dir = seg / seg_len; - var ext_a = a; - var ext_b = b; - if is_open_start { - ext_a = a - dir * half_width; - } - if is_open_end { - ext_b = b + dir * half_width; - } - let ext_result = dist_to_segment(point, ext_a, ext_b); - d_sq = ext_result.x; - } - } - // For non-open endpoints or when projection is on-segment, - // use the natural clamped distance (d_sq unchanged). - } - // CAP_ROUND: natural clamped distance produces round caps. No change needed. - - min_dist_sq = min(min_dist_sq, d_sq); - } - } - - let min_dist = sqrt(min_dist_sq); - - // Antialiased coverage. - let rasterization_mode = command.rasterization_mode; - if rasterization_mode == 1u { - // Aliased mode. - if min_dist <= half_width { - return 1.0; - } - return 0.0; - } - // Antialiased: smooth transition over 1 pixel at boundary. - return clamp(half_width + 0.5 - min_dist, 0.0, 1.0); - } - // ----------------------------------------------------------------------- // Main entry point // ----------------------------------------------------------------------- @@ -1011,31 +867,13 @@ fn cs_main( continue; } - // Branch: stroke mode vs fill mode. - let is_stroke = command.stroke_mode == 1u; - var coverage_value = 0.0; - if is_stroke { - // Stroke path: per-pixel distance-field evaluation. - if in_bounds && dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { - let half_width = u32_to_f32(command.stroke_half_width); - coverage_value = stroke_coverage( - dest_x_i32, dest_y_i32, command, - half_width, - command.stroke_line_cap, - command.stroke_line_join, - u32_to_f32(command.stroke_miter_limit), - tile_min_x, tile_min_y); - } - } else { - // Fill path: scanline rasterizer. - // Determine this tile's position in coverage-local space. + // Tile position in edge-local (coverage-local) space. let band_top = tile_min_y - command.edge_origin_y; let band_left_fixed = (tile_min_x - command.edge_origin_x) << FIXED_SHIFT; - // Band lookup: when edge_origin_y is 16-aligned the tile maps to one band; - // otherwise it can overlap two bands. + // Multi-band lookup: tile may overlap one or two bands. var first_band = band_top / 16; if band_top < 0 && (band_top % 16) != 0 { first_band -= 1; @@ -1082,7 +920,9 @@ fn cs_main( break; } let edge = edges[command.edge_start + b_start + ei]; - if min(edge.x0, edge.x1) >= tile_right_fixed { + if edge.y0 == edge.y1 { + // Skip degenerate edges (sentinel slots from stroke expand). + } else if min(edge.x0, edge.x1) >= tile_right_fixed { } else if max(edge.x0, edge.x1) < band_left_fixed { accumulate_start_cover(edge.y0, edge.y1, clip_top, clip_bottom, tile_top_fixed); } else { @@ -1093,7 +933,7 @@ fn cs_main( } workgroupBarrier(); - // Compute coverage for fill. + // Compute coverage. if in_bounds { if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y { var cover = atomicLoad(&tile_start_cover[py]); @@ -1104,7 +944,6 @@ fn cs_main( coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold)); } } - } // end fill path // Compose coverage result (shared by fill and stroke paths). if in_bounds && coverage_value > 0.0 { diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/StrokeExpandComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/StrokeExpandComputeShader.cs new file mode 100644 index 000000000..1d30116d7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/StrokeExpandComputeShader.cs @@ -0,0 +1,311 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU compute shader that expands stroke centerline edges into outline polygon edges. +/// Each thread processes one centerline edge and writes outline edges (side/join/cap) +/// to the output region of the edge buffer using an atomic counter for slot allocation. +/// The generated outline edges are then rasterized by the composite shader's fill path. +/// +internal static class StrokeExpandComputeShader +{ + private static readonly byte[] CodeBytes = + [ + .. """ + struct Edge { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + flags: i32, + adj_x: i32, + adj_y: i32, + } + + struct StrokeExpandCommand { + input_start: u32, + input_count: u32, + output_start: u32, + output_max: u32, + half_width: f32, + line_cap: u32, + line_join: u32, + miter_limit: f32, + } + + struct StrokeExpandConfig { + total_input_edges: u32, + command_count: u32, + } + + @group(0) @binding(0) var edges: array; + @group(0) @binding(1) var commands: array; + @group(0) @binding(2) var config: StrokeExpandConfig; + @group(0) @binding(3) var output_counters: array>; + + const FIXED_ONE: i32 = 256; + + // Edge descriptor flags (matches C# GpuEdge.Flags bit layout). + const EDGE_JOIN: i32 = 32; + const EDGE_CAP_START: i32 = 64; + const EDGE_CAP_END: i32 = 128; + + // LineCap enum values. + const CAP_BUTT: u32 = 0u; + const CAP_SQUARE: u32 = 1u; + const CAP_ROUND: u32 = 2u; + + // LineJoin enum values. + const JOIN_MITER: u32 = 0u; + const JOIN_MITER_REVERT: u32 = 1u; + const JOIN_ROUND: u32 = 2u; + const JOIN_BEVEL: u32 = 3u; + const JOIN_MITER_ROUND: u32 = 4u; + + var p_cmd: StrokeExpandCommand; + var p_cmd_idx: u32; + + fn emit_outline_edge(ex0: i32, ey0: i32, ex1: i32, ey1: i32) { + if ey0 == ey1 { return; } + let slot = atomicAdd(&output_counters[p_cmd_idx], 1u); + let idx = p_cmd.output_start + slot; + if idx >= p_cmd.output_max { return; } + var out_edge: Edge; + out_edge.x0 = ex0; + out_edge.y0 = ey0; + out_edge.x1 = ex1; + out_edge.y1 = ey1; + out_edge.flags = 0; + out_edge.adj_x = 0; + out_edge.adj_y = 0; + edges[idx] = out_edge; + } + + fn generate_side_edges(edge: Edge, hw_fp: f32) { + let fdx = f32(edge.x1 - edge.x0); + let fdy = f32(edge.y1 - edge.y0); + let flen = sqrt(fdx * fdx + fdy * fdy); + if flen < 1.0 { return; } + let nxf = -fdy / flen * hw_fp; + let nyf = fdx / flen * hw_fp; + let x0f = f32(edge.x0); + let y0f = f32(edge.y0); + let x1f = f32(edge.x1); + let y1f = f32(edge.y1); + emit_outline_edge( + i32(round(x0f + nxf)), i32(round(y0f + nyf)), + i32(round(x1f + nxf)), i32(round(y1f + nyf))); + emit_outline_edge( + i32(round(x1f - nxf)), i32(round(y1f - nyf)), + i32(round(x0f - nxf)), i32(round(y0f - nyf))); + } + + fn generate_join_edges(edge: Edge, hw_fp: f32, line_join: u32, miter_limit: f32) { + let vx = f32(edge.x0); + let vy = f32(edge.y0); + let dx1 = vx - f32(edge.x1); + let dy1 = vy - f32(edge.y1); + let len1 = sqrt(dx1 * dx1 + dy1 * dy1); + if len1 < 1.0 { return; } + let dx2 = f32(edge.adj_x) - vx; + let dy2 = f32(edge.adj_y) - vy; + let len2 = sqrt(dx2 * dx2 + dy2 * dy2); + if len2 < 1.0 { return; } + + let nx1 = -dy1 / len1; let ny1 = dx1 / len1; + let nx2 = -dy2 / len2; let ny2 = dx2 / len2; + let cross = dx1 * dy2 - dy1 * dx2; + + var oax: f32; var oay: f32; var obx: f32; var oby: f32; + var iax: f32; var iay: f32; var ibx: f32; var iby: f32; + if cross > 0.0 { + oax = vx - nx1 * hw_fp; oay = vy - ny1 * hw_fp; + obx = vx - nx2 * hw_fp; oby = vy - ny2 * hw_fp; + iax = vx + nx1 * hw_fp; iay = vy + ny1 * hw_fp; + ibx = vx + nx2 * hw_fp; iby = vy + ny2 * hw_fp; + } else { + oax = vx + nx1 * hw_fp; oay = vy + ny1 * hw_fp; + obx = vx + nx2 * hw_fp; oby = vy + ny2 * hw_fp; + iax = vx - nx1 * hw_fp; iay = vy - ny1 * hw_fp; + ibx = vx - nx2 * hw_fp; iby = vy - ny2 * hw_fp; + } + + var ofx: f32; var ofy: f32; var otx: f32; var oty: f32; + var ifx: f32; var ify: f32; var itx: f32; var ity: f32; + if cross > 0.0 { + ofx = obx; ofy = oby; otx = oax; oty = oay; + ifx = iax; ify = iay; itx = ibx; ity = iby; + } else { + ofx = oax; ofy = oay; otx = obx; oty = oby; + ifx = ibx; ify = iby; itx = iax; ity = iay; + } + + // Inner join: always bevel. + emit_outline_edge(i32(round(ifx)), i32(round(ify)), i32(round(itx)), i32(round(ity))); + + // Outer join. + var miter_handled = false; + if line_join == JOIN_MITER || line_join == JOIN_MITER_REVERT || line_join == JOIN_MITER_ROUND { + let ux1 = dx1 / len1; let uy1 = dy1 / len1; + let ux2 = dx2 / len2; let uy2 = dy2 / len2; + let denom = ux1 * uy2 - uy1 * ux2; + if abs(denom) > 1e-4 { + let dpx = obx - oax; let dpy = oby - oay; + let t = (dpx * uy2 - dpy * ux2) / denom; + let mx = oax + t * ux1; let my = oay + t * uy1; + let mdx = mx - vx; let mdy = my - vy; + let miter_dist = sqrt(mdx * mdx + mdy * mdy); + let limit = hw_fp * miter_limit; + if miter_dist <= limit { + emit_outline_edge(i32(round(ofx)), i32(round(ofy)), i32(round(mx)), i32(round(my))); + emit_outline_edge(i32(round(mx)), i32(round(my)), i32(round(otx)), i32(round(oty))); + miter_handled = true; + } else if line_join == JOIN_MITER { + let bdx = (oax + obx) * 0.5 - vx; + let bdy = (oay + oby) * 0.5 - vy; + let bdist = sqrt(bdx * bdx + bdy * bdy); + let blend = clamp((limit - bdist) / (miter_dist - bdist), 0.0, 1.0); + let cx1 = ofx + (mx - ofx) * blend; let cy1 = ofy + (my - ofy) * blend; + let cx2 = otx + (mx - otx) * blend; let cy2 = oty + (my - oty) * blend; + emit_outline_edge(i32(round(ofx)), i32(round(ofy)), i32(round(cx1)), i32(round(cy1))); + emit_outline_edge(i32(round(cx1)), i32(round(cy1)), i32(round(cx2)), i32(round(cy2))); + emit_outline_edge(i32(round(cx2)), i32(round(cy2)), i32(round(otx)), i32(round(oty))); + miter_handled = true; + } + } + } + if !miter_handled { + if line_join == JOIN_ROUND || line_join == JOIN_MITER_ROUND { + let sa = atan2(ofy - vy, ofx - vx); + let ea = atan2(oty - vy, otx - vx); + var sweep = ea - sa; + if sweep > 3.14159265 { sweep -= 2.0 * 3.14159265; } + if sweep < -3.14159265 { sweep += 2.0 * 3.14159265; } + let rpx = hw_fp / f32(FIXED_ONE); + let steps = max(4u, u32(ceil(abs(sweep) * rpx * 0.5))); + let da = sweep / f32(steps); + var pax = ofx; var pay = ofy; + for (var s = 1u; s <= steps; s++) { + var cax: f32; var cay: f32; + if s == steps { cax = otx; cay = oty; } + else { + let a = sa + da * f32(s); + cax = vx + cos(a) * hw_fp; + cay = vy + sin(a) * hw_fp; + } + emit_outline_edge(i32(round(pax)), i32(round(pay)), i32(round(cax)), i32(round(cay))); + pax = cax; pay = cay; + } + } else { + emit_outline_edge(i32(round(ofx)), i32(round(ofy)), i32(round(otx)), i32(round(oty))); + } + } + } + + fn generate_cap_edges(edge: Edge, hw_fp: f32, line_cap: u32, is_start: bool) { + let cx = f32(edge.x0); let cy = f32(edge.y0); + let ax = f32(edge.x1); let ay = f32(edge.y1); + var dx: f32; var dy: f32; + if is_start { dx = ax - cx; dy = ay - cy; } + else { dx = cx - ax; dy = cy - ay; } + let len = sqrt(dx * dx + dy * dy); + if len < 1.0 { return; } + let dir_x = dx / len; let dir_y = dy / len; + let nx = -dir_y * hw_fp; let ny = dir_x * hw_fp; + let lx = cx + nx; let ly = cy + ny; + let rx = cx - nx; let ry = cy - ny; + + if line_cap == CAP_BUTT { + if is_start { + emit_outline_edge(i32(round(rx)), i32(round(ry)), i32(round(lx)), i32(round(ly))); + } else { + emit_outline_edge(i32(round(lx)), i32(round(ly)), i32(round(rx)), i32(round(ry))); + } + } else if line_cap == CAP_SQUARE { + var ox: f32; var oy: f32; + if is_start { ox = -dir_x * hw_fp; oy = -dir_y * hw_fp; } + else { ox = dir_x * hw_fp; oy = dir_y * hw_fp; } + let lxe = lx + ox; let lye = ly + oy; + let rxe = rx + ox; let rye = ry + oy; + if is_start { + emit_outline_edge(i32(round(rx)), i32(round(ry)), i32(round(rxe)), i32(round(rye))); + emit_outline_edge(i32(round(rxe)), i32(round(rye)), i32(round(lxe)), i32(round(lye))); + emit_outline_edge(i32(round(lxe)), i32(round(lye)), i32(round(lx)), i32(round(ly))); + } else { + emit_outline_edge(i32(round(lx)), i32(round(ly)), i32(round(lxe)), i32(round(lye))); + emit_outline_edge(i32(round(lxe)), i32(round(lye)), i32(round(rxe)), i32(round(rye))); + emit_outline_edge(i32(round(rxe)), i32(round(rye)), i32(round(rx)), i32(round(ry))); + } + } else if line_cap == CAP_ROUND { + var sa: f32; var sx: f32; var sy: f32; var ex: f32; var ey: f32; + if is_start { + sa = atan2(ry - cy, rx - cx); + sx = rx; sy = ry; ex = lx; ey = ly; + } else { + sa = atan2(ly - cy, lx - cx); + sx = lx; sy = ly; ex = rx; ey = ry; + } + var sweep = atan2(ey - cy, ex - cx) - sa; + if sweep > 0.0 { sweep -= 2.0 * 3.14159265; } + let rpx = hw_fp / f32(FIXED_ONE); + let steps = max(4u, u32(ceil(abs(sweep) * rpx * 0.5))); + let da = sweep / f32(steps); + var pax = sx; var pay = sy; + for (var s = 1u; s <= steps; s++) { + var cax: f32; var cay: f32; + if s == steps { cax = ex; cay = ey; } + else { + let a = sa + da * f32(s); + cax = cx + cos(a) * hw_fp; + cay = cy + sin(a) * hw_fp; + } + emit_outline_edge(i32(round(pax)), i32(round(pay)), i32(round(cax)), i32(round(cay))); + pax = cax; pay = cay; + } + } + } + + @compute @workgroup_size(256, 1, 1) + fn cs_main(@builtin(global_invocation_id) gid: vec3) { + let thread_idx = gid.x; + if thread_idx >= config.total_input_edges { return; } + + // Find which command this thread's edge belongs to. + var running = 0u; + var cmd_idx = 0u; + for (var c = 0u; c < config.command_count; c++) { + let cmd = commands[c]; + if thread_idx < running + cmd.input_count { + cmd_idx = c; + break; + } + running += cmd.input_count; + } + + let cmd = commands[cmd_idx]; + p_cmd = cmd; + p_cmd_idx = cmd_idx; + let local_idx = thread_idx - running; + let edge = edges[cmd.input_start + local_idx]; + let hw_fp = cmd.half_width * f32(FIXED_ONE); + let flags = edge.flags; + + if (flags & EDGE_JOIN) != 0 { + generate_join_edges(edge, hw_fp, cmd.line_join, cmd.miter_limit); + } else if (flags & EDGE_CAP_START) != 0 { + generate_cap_edges(edge, hw_fp, cmd.line_cap, true); + } else if (flags & EDGE_CAP_END) != 0 { + generate_cap_edges(edge, hw_fp, cmd.line_cap, false); + } else { + generate_side_edges(edge, hw_fp); + } + } + """u8, + 0 + ]; + + /// Gets the WGSL source for this shader as a null-terminated UTF-8 span. + public static ReadOnlySpan Code => CodeBytes; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 140718a4c..2df64d1c5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; public sealed unsafe partial class WebGPUDrawingBackend { private const int TileHeight = 16; - private const int EdgeStrideBytes = 20; + private const int EdgeStrideBytes = 28; private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private const int CsrWorkgroupSize = 256; @@ -35,9 +35,25 @@ public sealed unsafe partial class WebGPUDrawingBackend private delegate void ComputePassDispatch(ComputePassEncoder* pass); /// - /// Builds flattened fixed-point edge geometry for all coverage definitions and uploads to a GPU buffer. - /// Each edge is in 24.8 fixed-point format with min_row/max_row and CSR metadata. + /// Computes the maximum number of outline edges a single centerline edge can produce + /// for the given stroke parameters. The worst case is a round join or cap where + /// the arc step count scales with stroke width: max(4, ceil(π * halfWidth * 0.5)). /// + private static int ComputeOutlineEdgesPerCenterline(float halfWidth, LineJoin lineJoin, LineCap lineCap) + { + // Side edges always produce exactly 2. + // Join: 1 inner bevel + outer edges (miter variants: up to 3, bevel: 1, round: arc steps). + // Cap: butt=1, square=3, round=arc steps. + int roundArcSteps = Math.Max(4, (int)MathF.Ceiling(MathF.PI * halfWidth * 0.5f)); + int maxJoin = lineJoin is LineJoin.Round or LineJoin.MiterRound + ? 1 + roundArcSteps + : 4; // miter clamp worst case: 1 inner + 3 outer + int maxCap = lineCap == LineCap.Round + ? roundArcSteps + : 3; // square cap + return Math.Max(Math.Max(maxJoin, maxCap), 2); + } + private bool TryCreateEdgeBuffer( WebGPUFlushContext flushContext, List definitions, @@ -49,6 +65,7 @@ private bool TryCreateEdgeBuffer( out int totalBandOffsetEntries, out WgpuBuffer* bandOffsetsBuffer, out nuint bandOffsetsBufferSize, + out StrokeExpandInfo strokeExpandInfo, out string? error) where TPixel : unmanaged, IPixel { @@ -59,6 +76,7 @@ private bool TryCreateEdgeBuffer( totalBandOffsetEntries = 0; bandOffsetsBuffer = null; bandOffsetsBufferSize = 0; + strokeExpandInfo = default; error = null; if (definitions.Count == 0) { @@ -71,6 +89,12 @@ private bool TryCreateEdgeBuffer( // Build pre-split geometry for each definition and compute edge placements. DefinitionGeometry[] geometries = new DefinitionGeometry[definitions.Count]; + + // Track stroke definitions for the expand dispatch. + List? strokeCommands = null; + int totalStrokeCenterlineEdges = 0; + int totalOutlineSlots = 0; + for (int i = 0; i < definitions.Count; i++) { CompositionCoverageDefinition definition = definitions[i]; @@ -82,13 +106,8 @@ private bool TryCreateEdgeBuffer( } uint fillRule = definition.RasterizerOptions.IntersectionRule == IntersectionRule.EvenOdd ? 1u : 0u; - int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); - // For stroke definitions, expand band assignment so distance-field - // lookups can find nearby edges beyond the edge's own Y range. - int strokeExpand = definition.IsStroke - ? (int)MathF.Ceiling(definition.StrokeWidth * 0.5f) + 1 - : 0; + int bandCount = (int)DivideRoundUp(interest.Height, TileHeight); IMemoryOwner? defEdgeOwner; int edgeCount; @@ -101,14 +120,13 @@ private bool TryCreateEdgeBuffer( definition.Path, in interest, definition.RasterizerOptions.SamplingOrigin, - definition.StrokeWidth * 0.5f, - definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel, + definition.StrokeWidth, (float)(definition.StrokeOptions?.MiterLimit ?? 4.0), + bandCount, out defEdgeOwner, out edgeCount, out defBandOffsets, - out error, - strokeExpand); + out error); } else { @@ -125,7 +143,6 @@ private bool TryCreateEdgeBuffer( if (!edgeSuccess) { - // Dispose any already-built geometry on failure. for (int j = 0; j < i; j++) { geometries[j].EdgeOwner?.Dispose(); @@ -136,35 +153,144 @@ private bool TryCreateEdgeBuffer( geometries[i] = new DefinitionGeometry(defEdgeOwner, edgeCount, bandCount, defBandOffsets); - int bandOffsetEntriesForDef = bandCount + 1; - uint strokeLineCap = definition.IsStroke ? (uint)(definition.StrokeOptions?.LineCap ?? LineCap.Butt) : 0; - uint strokeLineJoin = definition.IsStroke ? (uint)(definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel) : 0; - float strokeMiterLimit = definition.IsStroke ? (float)(definition.StrokeOptions?.MiterLimit ?? 4.0) : 0f; - - edgePlacements[i] = new EdgePlacement( - (uint)runningEdgeStart, - (uint)edgeCount, - fillRule, - (uint)runningBandOffset, - (uint)bandCount, - definition.IsStroke, - definition.StrokeWidth, - strokeLineCap, - strokeLineJoin, - strokeMiterLimit); - - runningEdgeStart += edgeCount; - runningBandOffset += bandOffsetEntriesForDef; + if (definition.IsStroke && edgeCount > 0) + { + // Centerline edges are band-sorted. Create one StrokeExpandCommand per band + // so the GPU expand shader writes outline edges into per-band output slots. + // This produces band-sorted outline edges compatible with the fill rasterizer. + uint[] clBandOffsets = defBandOffsets!; + LineCap defLineCap = definition.StrokeOptions?.LineCap ?? LineCap.Butt; + LineJoin defLineJoin = definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel; + int outlineEdgesPerCenterline = ComputeOutlineEdgesPerCenterline( + definition.StrokeWidth * 0.5f, defLineJoin, defLineCap); + strokeCommands ??= []; + for (int b = 0; b < bandCount; b++) + { + uint bandStart = clBandOffsets[b]; + uint bandEnd = b + 1 < clBandOffsets.Length ? clBandOffsets[b + 1] : (uint)edgeCount; + uint bandEdgeCount = bandEnd - bandStart; + if (bandEdgeCount == 0) + { + continue; + } + + int bandOutlineMax = (int)bandEdgeCount * outlineEdgesPerCenterline; + strokeCommands.Add(new StrokeExpandCommand( + i, + (uint)runningEdgeStart + bandStart, + bandEdgeCount, + definition.StrokeWidth * 0.5f, + (uint)defLineCap, + (uint)defLineJoin, + (float)(definition.StrokeOptions?.MiterLimit ?? 4.0), + bandOutlineMax, + Band: b)); + + totalOutlineSlots += bandOutlineMax; + } + + totalStrokeCenterlineEdges += edgeCount; + + // Placeholder EdgePlacement — will be updated after outline space is allocated. + int bandOffsetEntriesForDef = bandCount + 1; + edgePlacements[i] = new EdgePlacement( + (uint)runningEdgeStart, + (uint)edgeCount, + fillRule, + (uint)runningBandOffset, + (uint)bandCount); + + runningEdgeStart += edgeCount; + runningBandOffset += bandOffsetEntriesForDef; + } + else + { + int bandOffsetEntriesForDef = bandCount + 1; + + edgePlacements[i] = new EdgePlacement( + (uint)runningEdgeStart, + (uint)edgeCount, + fillRule, + (uint)runningBandOffset, + (uint)bandCount); + + runningEdgeStart += edgeCount; + runningBandOffset += bandOffsetEntriesForDef; + } } totalEdgeCount = runningEdgeStart; totalBandOffsetEntries = runningBandOffset; + // Reserve outline edge space for stroke definitions. + // Outline edges are placed after all centerline/fill edges in the buffer. + // Each band within a stroke definition gets its own output slot so the + // resulting outline edges are band-sorted — compatible with the fill rasterizer. + // outlineBandOffsetsPerDef[defIndex] stores the outline band offsets array for + // stroke definitions (null for fills). Used in the merged band offsets upload. + uint[]?[] outlineBandOffsetsPerDef = new uint[]?[definitions.Count]; + int outlineRegionStart = totalEdgeCount; + if (strokeCommands is not null) + { + // Assign per-command output offsets and build per-definition outline band offsets. + int runningOutlineOffset = outlineRegionStart; + for (int sc = 0; sc < strokeCommands.Count; sc++) + { + StrokeExpandCommand cmd = strokeCommands[sc]; + strokeCommands[sc] = cmd with + { + OutputStart = (uint)runningOutlineOffset, + OutputMax = (uint)(runningOutlineOffset + cmd.OutlineMax) + }; + runningOutlineOffset += cmd.OutlineMax; + } + + totalEdgeCount = runningOutlineOffset; + + // Build outline band offsets for each stroke definition. + // Commands are ordered by definition, then by band within each definition. + int cmdCursor = 0; + while (cmdCursor < strokeCommands.Count) + { + int defIndex = strokeCommands[cmdCursor].DefinitionIndex; + int defBandCount = geometries[defIndex].BandCount; + int defOutlineStart = (int)strokeCommands[cmdCursor].OutputStart; + + // Build full band offsets: offsets[b] = local offset within this def's outline region. + uint[] outOffsets = new uint[defBandCount + 1]; + uint localOffset = 0; + for (int b = 0; b < defBandCount; b++) + { + outOffsets[b] = localOffset; + if (cmdCursor < strokeCommands.Count + && strokeCommands[cmdCursor].DefinitionIndex == defIndex + && strokeCommands[cmdCursor].Band == b) + { + localOffset += (uint)strokeCommands[cmdCursor].OutlineMax; + cmdCursor++; + } + } + + int defOutlineTotal = (int)localOffset; + outOffsets[defBandCount] = (uint)defOutlineTotal; + outlineBandOffsetsPerDef[defIndex] = outOffsets; + + // Update EdgePlacement to point to the outline region. + EdgePlacement oldPlacement = edgePlacements[defIndex]; + edgePlacements[defIndex] = new EdgePlacement( + (uint)defOutlineStart, + (uint)defOutlineTotal, + oldPlacement.FillRule, + (uint)runningBandOffset, + (uint)defBandCount); + + runningBandOffset += defBandCount + 1; + totalBandOffsetEntries = runningBandOffset; + } + } + if (totalEdgeCount == 0) { - // Provide properly sized buffers even when there are no edges. - // The shader reads band_offsets[bandOffsetsStart + band], so the - // buffer must be large enough and zeroed. edgeBufferSize = EdgeStrideBytes; int emptyOffsetsCount = Math.Max(totalBandOffsetEntries, 1); bandOffsetsBufferSize = checked((nuint)(emptyOffsetsCount * sizeof(uint))); @@ -178,21 +304,21 @@ private bool TryCreateEdgeBuffer( return true; } - // Merge edge arrays. Edges are already pre-split and band-sorted per definition. - // For single-definition scenes (common case), use the buffer directly. + // Merge edge arrays. Includes space for outline edges at the end. int edgeBufferBytes = checked(totalEdgeCount * EdgeStrideBytes); edgeBufferSize = (nuint)edgeBufferBytes; + // Upload only the centerline/fill edges; outline region will be written by GPU. + int uploadEdgeCount = outlineRegionStart; + IMemoryOwner? mergedEdgeOwner; - if (geometries.Length == 1 && geometries[0].EdgeOwner is not null) + if (geometries.Length == 1 && geometries[0].EdgeOwner is not null && strokeCommands is null) { - // Single definition: use directly (no per-edge metadata to stamp). mergedEdgeOwner = geometries[0].EdgeOwner!; } else { - // Multiple definitions: concatenate into a new buffer. - mergedEdgeOwner = flushContext.MemoryAllocator.Allocate(totalEdgeCount); + mergedEdgeOwner = flushContext.MemoryAllocator.Allocate(uploadEdgeCount > 0 ? uploadEdgeCount : 1); int mergedEdgeIndex = 0; for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { @@ -202,16 +328,13 @@ private bool TryCreateEdgeBuffer( continue; } - ReadOnlySpan source = geometry.EdgeOwner.Memory.Span[..geometry.EdgeCount]; + ReadOnlySpan source = geometry.EdgeOwner.Memory.Span; Span dest = mergedEdgeOwner.Memory.Span.Slice(mergedEdgeIndex, geometry.EdgeCount); source.CopyTo(dest); mergedEdgeIndex += geometry.EdgeCount; } } - // Reinterpret typed buffer as bytes for GPU upload. - Span edgeUpload = MemoryMarshal.AsBytes(mergedEdgeOwner.Memory.Span[..totalEdgeCount]); - if (!TryGetOrCreateCoverageBuffer( flushContext, "coverage-aggregated-edges", @@ -224,20 +347,32 @@ private bool TryCreateEdgeBuffer( return false; } - if (!this.TryUploadDirtyCoverageRange( - flushContext, - edgeBuffer, - edgeUpload, - ref this.cachedCoverageLineUpload, - ref this.cachedCoverageLineLength, - out error)) + // Upload centerline/fill edges to the beginning of the buffer. + if (uploadEdgeCount > 0) { - DisposeGeometries(geometries, mergedEdgeOwner); - return false; + Span edgeUpload = MemoryMarshal.AsBytes(mergedEdgeOwner.Memory.Span); + if (!this.TryUploadDirtyCoverageRange( + flushContext, + edgeBuffer, + edgeUpload, + ref this.cachedCoverageLineUpload, + ref this.cachedCoverageLineLength, + out error)) + { + DisposeGeometries(geometries, mergedEdgeOwner); + return false; + } + } + + // Clear the outline region so unused slots have y0 == y1 == 0 (no winding contribution). + if (totalOutlineSlots > 0) + { + nuint outlineByteOffset = (nuint)(outlineRegionStart * EdgeStrideBytes); + nuint outlineByteSize = (nuint)(totalOutlineSlots * EdgeStrideBytes); + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, edgeBuffer, outlineByteOffset, outlineByteSize); } // Build merged band offsets from pre-computed per-definition data. - // Band offsets are local to each definition's edge range (0-based). int bandOffsetsCount = Math.Max(totalBandOffsetEntries, 1); bandOffsetsBufferSize = checked((nuint)(bandOffsetsCount * sizeof(uint))); @@ -249,27 +384,27 @@ private bool TryCreateEdgeBuffer( if (totalEdgeCount > 0 && totalBandOffsetEntries > 0) { - uint[] mergedOffsets = new uint[totalBandOffsetEntries]; + using IMemoryOwner mergedOffsetsOwner = flushContext.MemoryAllocator.Allocate(totalBandOffsetEntries, AllocationOptions.Clean); + Span mergedOffsets = mergedOffsetsOwner.Memory.Span; for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { ref DefinitionGeometry geometry = ref geometries[defIndex]; - if (geometry.BandOffsets is null) - { - continue; - } - EdgePlacement placement = edgePlacements[defIndex]; int bandStart = (int)placement.CsrOffsetsStart; - uint[] defOffsets = geometry.BandOffsets; - // Copy band offsets directly (already 0-based per definition). - for (int b = 0; b < defOffsets.Length; b++) + // Use outline band offsets for stroke definitions, fill band offsets otherwise. + uint[]? outlineOffsets = outlineBandOffsetsPerDef[defIndex]; + uint[]? defOffsets = outlineOffsets ?? geometry.BandOffsets; + if (defOffsets is not null) { - mergedOffsets[bandStart + b] = defOffsets[b]; + for (int b = 0; b < defOffsets.Length; b++) + { + mergedOffsets[bandStart + b] = defOffsets[b]; + } } } - fixed (uint* offsetsPtr = mergedOffsets) + fixed (uint* offsetsPtr = &MemoryMarshal.GetReference(mergedOffsets)) { flushContext.Api.QueueWriteBuffer( flushContext.Queue, @@ -284,6 +419,12 @@ private bool TryCreateEdgeBuffer( flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, bandOffsetsBuffer, 0, bandOffsetsBufferSize); } + // Build stroke expand info for the GPU dispatch. + if (strokeCommands is not null && strokeCommands.Count > 0) + { + strokeExpandInfo = new StrokeExpandInfo(strokeCommands, totalStrokeCenterlineEdges); + } + DisposeGeometries(geometries, mergedEdgeOwner); error = null; @@ -462,7 +603,8 @@ private static bool TryBuildFixedPointEdges( // Pass 1: Flatten path and count edges per band. int totalSubEdges = 0; - int[] bandCounts = new int[bandCount]; + using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span bandCounts = bandCountsOwner.Memory.Span; foreach (ISimplePath simplePath in path.Flatten()) { @@ -525,7 +667,8 @@ private static bool TryBuildFixedPointEdges( // Pass 2: Flatten again and scatter edges directly into the final buffer. IMemoryOwner finalOwner = allocator.Allocate(totalSubEdges); Span finalSpan = finalOwner.Memory.Span; - uint[] writeCursors = new uint[bandCount]; + using IMemoryOwner writeCursorsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span writeCursors = writeCursorsOwner.Memory.Span; foreach (ISimplePath simplePath in path.Flatten()) { @@ -582,23 +725,23 @@ private static bool TryBuildFixedPointEdges( } /// - /// Builds stroke-specific fixed-point edge geometry with cap flags and miter extensions. - /// Unlike the fill edge builder, this includes horizontal edges and computes - /// per-vertex miter extensions for Miter/MiterRevert/MiterRound join types. + /// Builds stroke centerline edges with join and cap descriptors for GPU-side outline generation. + /// The GPU shader expands centerline edges into outline polygon edges and rasterizes them + /// using the same fill rasterizer. Edges are band-sorted with expanded Y ranges to account + /// for stroke offset so each tile only processes edges relevant to its vertical range. /// private static bool TryBuildStrokeEdges( MemoryAllocator allocator, IPath path, in Rectangle interest, RasterizerSamplingOrigin samplingOrigin, - float halfWidth, - LineJoin lineJoin, + float strokeWidth, float miterLimit, + int bandCount, out IMemoryOwner? edgeOwner, out int edgeCount, out uint[]? bandOffsets, - out string? error, - int strokeExpandPixels = 0) + out string? error) { error = null; edgeOwner = null; @@ -607,15 +750,17 @@ private static bool TryBuildStrokeEdges( bool samplePixelCenter = samplingOrigin == RasterizerSamplingOrigin.PixelCenter; float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; - int height = interest.Height; int interestX = interest.X; int interestY = interest.Y; - int bandCount = (int)DivideRoundUp(height, TileHeight); - bool isMiterJoin = lineJoin is LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound; + int height = interest.Height; + + // Maximum Y expansion in pixels: miter joins can extend up to miterLimit * halfWidth. + float halfWidth = strokeWidth * 0.5f; + int yExpansionFixed = (int)MathF.Ceiling(Math.Max(miterLimit, 1f) * halfWidth * FixedOne); - // Pre-process: flatten all sub-paths and compute stroke edges with - // miter extensions and endpoint flags. + // Pass 1: Collect all stroke edges and count per band. List strokeEdges = []; + List<(int YMinFixed, int YMaxFixed)> edgeYRanges = []; foreach (ISimplePath simplePath in path.Flatten()) { @@ -632,76 +777,92 @@ private static bool TryBuildStrokeEdges( continue; } - // Pre-compute per-vertex miter extensions. - // extensions[j] is the amount to extend the outgoing segment backward - // (and the incoming segment forward) at vertex j. - float[] extensions = new float[points.Length]; - if (isMiterJoin) - { - ComputeMiterExtensions(points, isClosed, halfWidth, miterLimit, lineJoin, extensions); - } - + // Emit centerline edges. for (int j = 0; j < segmentCount; j++) { int j1 = j + 1 == points.Length ? 0 : j + 1; PointF p0 = points[j]; PointF p1 = points[j1]; - // Apply miter extensions at both endpoints. - float ext0 = extensions[j]; // extension at start vertex (forward along this segment) - float ext1 = extensions[j1]; // extension at end vertex (backward along this segment) + float fy0 = (p0.Y - interestY) + samplingOffsetY; + float fy1 = (p1.Y - interestY) + samplingOffsetY; + int iy0 = (int)MathF.Round(fy0 * FixedOne); + int iy1 = (int)MathF.Round(fy1 * FixedOne); - if (ext0 > 0f || ext1 > 0f) + strokeEdges.Add(new GpuEdge { - float dx = p1.X - p0.X; - float dy = p1.Y - p0.Y; - float segLen = MathF.Sqrt((dx * dx) + (dy * dy)); - if (segLen > 1e-6f) - { - float invLen = 1f / segLen; - float dirX = dx * invLen; - float dirY = dy * invLen; - - // Extend start backward (away from p1). - if (ext0 > 0f) - { - p0 = new PointF(p0.X - (dirX * ext0), p0.Y - (dirY * ext0)); - } - - // Extend end forward (away from p0). - if (ext1 > 0f) - { - p1 = new PointF(p1.X + (dirX * ext1), p1.Y + (dirY * ext1)); - } - } - } + X0 = (int)MathF.Round(((p0.X - interestX) + samplingOffsetX) * FixedOne), + Y0 = iy0, + X1 = (int)MathF.Round(((p1.X - interestX) + samplingOffsetX) * FixedOne), + Y1 = iy1, + }); + + int eMin = Math.Min(iy0, iy1) - yExpansionFixed; + int eMax = Math.Max(iy0, iy1) + yExpansionFixed; + edgeYRanges.Add((eMin, eMax)); + } + + // Emit join descriptors at interior vertices. + int startVertex = isClosed ? 0 : 1; + int endVertex = isClosed ? points.Length : points.Length - 1; + + for (int i = startVertex; i < endVertex; i++) + { + int prev = isClosed ? ((i - 1 + points.Length) % points.Length) : i - 1; + int next = isClosed ? ((i + 1) % points.Length) : i + 1; - // Compute cap flags. - int flags = 0; - if (!isClosed) + PointF v = points[i]; + PointF pv = points[prev]; + PointF nv = points[next]; + + int vy = (int)MathF.Round(((v.Y - interestY) + samplingOffsetY) * FixedOne); + + strokeEdges.Add(new GpuEdge { - if (j == 0) - { - flags |= 1; // open start at (x0, y0) - } + X0 = (int)MathF.Round(((v.X - interestX) + samplingOffsetX) * FixedOne), + Y0 = vy, + X1 = (int)MathF.Round(((pv.X - interestX) + samplingOffsetX) * FixedOne), + Y1 = (int)MathF.Round(((pv.Y - interestY) + samplingOffsetY) * FixedOne), + Flags = 32, // EDGE_JOIN + AdjX = (int)MathF.Round(((nv.X - interestX) + samplingOffsetX) * FixedOne), + AdjY = (int)MathF.Round(((nv.Y - interestY) + samplingOffsetY) * FixedOne), + }); + + edgeYRanges.Add((vy - yExpansionFixed, vy + yExpansionFixed)); + } - if (j == segmentCount - 1) - { - flags |= 2; // open end at (x1, y1) - } - } + // Emit cap descriptors at open endpoints. + if (!isClosed) + { + PointF capStart = points[0]; + PointF adjStart = points[1]; + int csy = (int)MathF.Round(((capStart.Y - interestY) + samplingOffsetY) * FixedOne); - float fx0 = (p0.X - interestX) + samplingOffsetX; - float fy0 = (p0.Y - interestY) + samplingOffsetY; - float fx1 = (p1.X - interestX) + samplingOffsetX; - float fy1 = (p1.Y - interestY) + samplingOffsetY; + strokeEdges.Add(new GpuEdge + { + X0 = (int)MathF.Round(((capStart.X - interestX) + samplingOffsetX) * FixedOne), + Y0 = csy, + X1 = (int)MathF.Round(((adjStart.X - interestX) + samplingOffsetX) * FixedOne), + Y1 = (int)MathF.Round(((adjStart.Y - interestY) + samplingOffsetY) * FixedOne), + Flags = 64, // EDGE_CAP_START + }); - int x0 = (int)MathF.Round(fx0 * FixedOne); - int y0 = (int)MathF.Round(fy0 * FixedOne); - int x1 = (int)MathF.Round(fx1 * FixedOne); - int y1 = (int)MathF.Round(fy1 * FixedOne); + edgeYRanges.Add((csy - yExpansionFixed, csy + yExpansionFixed)); + + PointF capEnd = points[^1]; + PointF adjEnd = points[^2]; + int cey = (int)MathF.Round(((capEnd.Y - interestY) + samplingOffsetY) * FixedOne); - strokeEdges.Add(new GpuEdge { X0 = x0, Y0 = y0, X1 = x1, Y1 = y1, Flags = flags }); + strokeEdges.Add(new GpuEdge + { + X0 = (int)MathF.Round(((capEnd.X - interestX) + samplingOffsetX) * FixedOne), + Y0 = cey, + X1 = (int)MathF.Round(((adjEnd.X - interestX) + samplingOffsetX) * FixedOne), + Y1 = (int)MathF.Round(((adjEnd.Y - interestY) + samplingOffsetY) * FixedOne), + Flags = 128, // EDGE_CAP_END + }); + + edgeYRanges.Add((cey - yExpansionFixed, cey + yExpansionFixed)); } } @@ -710,28 +871,29 @@ private static bool TryBuildStrokeEdges( return true; } - // Count edges per band (including horizontal edges, with stroke expansion). - int[] bandCounts = new int[bandCount]; - int totalSubEdges = 0; + // Band-sort centerline edges using expanded Y ranges so each band contains + // all centerline edges whose outline could affect that band's vertical range. + // This mirrors TryBuildFixedPointEdges but uses pre-computed Y expansion. + using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span bandCounts = bandCountsOwner.Memory.Span; + int totalBandEdges = 0; for (int i = 0; i < strokeEdges.Count; i++) { - GpuEdge edge = strokeEdges[i]; - ComputeStrokeBandRange(edge, height, strokeExpandPixels, out int minBand, out int maxBand); - if (minBand > maxBand) - { - continue; - } - + (int yMin, int yMax) = edgeYRanges[i]; + int minRow = Math.Max(0, yMin >> FixedShift); + int maxRow = Math.Min(height - 1, Math.Max(0, (yMax - 1) >> FixedShift)); + int minBand = Math.Min(minRow / TileHeight, bandCount - 1); + int maxBand = Math.Min(maxRow / TileHeight, bandCount - 1); for (int b = minBand; b <= maxBand; b++) { bandCounts[b]++; } - totalSubEdges += maxBand - minBand + 1; + totalBandEdges += maxBand - minBand + 1; } - if (totalSubEdges == 0) + if (totalBandEdges == 0) { return true; } @@ -747,20 +909,20 @@ private static bool TryBuildStrokeEdges( offsets[bandCount] = running; - // Scatter edges into band-sorted buffer. - IMemoryOwner finalOwner = allocator.Allocate(totalSubEdges); + // Scatter centerline edges into band-sorted buffer. + IMemoryOwner finalOwner = allocator.Allocate(totalBandEdges); Span finalSpan = finalOwner.Memory.Span; - uint[] writeCursors = new uint[bandCount]; + using IMemoryOwner writeCursorsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span writeCursors = writeCursorsOwner.Memory.Span; for (int i = 0; i < strokeEdges.Count; i++) { GpuEdge edge = strokeEdges[i]; - ComputeStrokeBandRange(edge, height, strokeExpandPixels, out int minBand, out int maxBand); - if (minBand > maxBand) - { - continue; - } - + (int yMin, int yMax) = edgeYRanges[i]; + int minRow = Math.Max(0, yMin >> FixedShift); + int maxRow = Math.Min(height - 1, Math.Max(0, (yMax - 1) >> FixedShift)); + int minBand = Math.Min(minRow / TileHeight, bandCount - 1); + int maxBand = Math.Min(maxRow / TileHeight, bandCount - 1); for (int band = minBand; band <= maxBand; band++) { finalSpan[(int)(offsets[band] + writeCursors[band])] = edge; @@ -769,164 +931,11 @@ private static bool TryBuildStrokeEdges( } edgeOwner = finalOwner; - edgeCount = totalSubEdges; + edgeCount = totalBandEdges; bandOffsets = offsets; return true; } - /// - /// Computes band range for a stroke edge, including horizontal edges and stroke expansion. - /// - private static void ComputeStrokeBandRange(in GpuEdge edge, int height, int strokeExpandPixels, out int minBand, out int maxBand) - { - int y0 = edge.Y0; - int y1 = edge.Y1; - - int yMinFixed, yMaxFixed; - if (y0 == y1) - { - // Horizontal edge: use the row at this Y position. - yMinFixed = y0; - yMaxFixed = y0 + 1; // ensure at least one row - } - else - { - yMinFixed = Math.Min(y0, y1); - yMaxFixed = Math.Max(y0, y1); - } - - int minRow = Math.Max(0, yMinFixed >> FixedShift); - int maxRow = Math.Min(height - 1, (yMaxFixed - 1) >> FixedShift); - - // For horizontal edges the row range can be empty; use the Y row directly. - if (y0 == y1) - { - int row = Math.Clamp(y0 >> FixedShift, 0, height - 1); - minRow = row; - maxRow = row; - } - - if (strokeExpandPixels > 0) - { - minRow = Math.Max(0, minRow - strokeExpandPixels); - maxRow = Math.Min(height - 1, maxRow + strokeExpandPixels); - } - - if (minRow > maxRow) - { - minBand = 1; - maxBand = 0; // empty range sentinel - return; - } - - minBand = minRow / TileHeight; - maxBand = maxRow / TileHeight; - } - - /// - /// Pre-computes miter extension lengths at each vertex of a sub-path. - /// At each interior vertex where two segments meet, the extension is - /// halfWidth / tan(halfAngle), clamped by the miter limit. - /// - private static void ComputeMiterExtensions( - ReadOnlySpan points, - bool isClosed, - float halfWidth, - float miterLimit, - LineJoin lineJoin, - float[] extensions) - { - int n = points.Length; - - for (int i = 0; i < n; i++) - { - extensions[i] = 0f; - } - - // For each interior vertex, compute the miter extension. - int startVertex = isClosed ? 0 : 1; - int endVertex = isClosed ? n : n - 1; - float limit = halfWidth * miterLimit; - - for (int i = startVertex; i < endVertex; i++) - { - int prev = isClosed ? (i - 1 + n) % n : i - 1; - int next = isClosed ? (i + 1) % n : i + 1; - - float dx1 = points[i].X - points[prev].X; - float dy1 = points[i].Y - points[prev].Y; - float dx2 = points[next].X - points[i].X; - float dy2 = points[next].Y - points[i].Y; - - float len1 = MathF.Sqrt((dx1 * dx1) + (dy1 * dy1)); - float len2 = MathF.Sqrt((dx2 * dx2) + (dy2 * dy2)); - if (len1 < 1e-6f || len2 < 1e-6f) - { - continue; - } - - // Normalize directions. - float ux1 = dx1 / len1; - float uy1 = dy1 / len1; - float ux2 = dx2 / len2; - float uy2 = dy2 / len2; - - // Dot product of directions gives cos(angle). - float dot = (ux1 * ux2) + (uy1 * uy2); - dot = Math.Clamp(dot, -1f, 1f); - - // Half-angle: cos(θ) = dot → θ = acos(dot) → half = θ/2. - float angle = MathF.Acos(dot); - float halfAngle = angle * 0.5f; - float sinHalf = MathF.Sin(halfAngle); - - if (sinHalf < 1e-6f) - { - // Near-parallel segments (straight line), no miter needed. - continue; - } - - float cosHalf = MathF.Cos(halfAngle); - float tanHalf = sinHalf / cosHalf; - if (tanHalf < 1e-6f) - { - continue; - } - - // Full extension along each segment = halfWidth / tan(halfAngle). - float ext = halfWidth / tanHalf; - - // Miter distance from vertex = halfWidth / sin(halfAngle). - float miterDistance = halfWidth / sinHalf; - - if (miterDistance <= limit) - { - // Within miter limit: full extension for all join types. - extensions[i] = ext; - } - else - { - // Miter limit exceeded: behavior depends on join type. - switch (lineJoin) - { - case LineJoin.Miter: - // Clip the miter at the limit distance. - // The clipped extension is the projection of the clipped point onto the segment. - extensions[i] = limit * cosHalf; - break; - - case LineJoin.MiterRevert: - // Bevel fallback: no extension needed. - break; - - case LineJoin.MiterRound: - // Round fallback: natural SDF handles it, no extension needed. - break; - } - } - } - } - /// /// Creates and executes a compute pass for a coverage pipeline stage. /// @@ -1185,8 +1194,16 @@ private struct GpuEdge /// Bit flags for stroke edge metadata. /// Bit 0: open start — the (X0,Y0) endpoint is an open path start (cap applies). /// Bit 1: open end — the (X1,Y1) endpoint is an open path end (cap applies). + /// Bit 2: bevel fill — this edge is a bevel fill chord (AdjX,AdjY = join vertex). /// public int Flags; + + /// + /// Auxiliary coordinates (fixed-point). For bevel fill edges, stores the + /// join vertex V so the shader can compute the bevel triangle SDF. + /// + public int AdjX; + public int AdjY; } /// @@ -1213,4 +1230,82 @@ public DefinitionGeometry( this.BandOffsets = bandOffsets; } } + + /// + /// Describes a stroke expand command for the GPU shader. + /// Each command expands one coverage definition's centerline edges into outline edges. + /// + private record struct StrokeExpandCommand( + int DefinitionIndex, + uint InputStart, + uint InputCount, + float HalfWidth, + uint LineCap, + uint LineJoin, + float MiterLimit, + int OutlineMax, + uint OutputStart = 0, + uint OutputMax = 0, + int Band = 0); + + /// + /// GPU-side stroke expand command matching the WGSL StrokeExpandCommand struct layout. + /// + [StructLayout(LayoutKind.Sequential)] + private readonly struct GpuStrokeExpandCommand + { + public readonly uint InputStart; + public readonly uint InputCount; + public readonly uint OutputStart; + public readonly uint OutputMax; + public readonly uint HalfWidth; // f32 as bits + public readonly uint LineCap; + public readonly uint LineJoin; + public readonly uint MiterLimit; // f32 as bits + + public GpuStrokeExpandCommand(StrokeExpandCommand cmd) + { + this.InputStart = cmd.InputStart; + this.InputCount = cmd.InputCount; + this.OutputStart = cmd.OutputStart; + this.OutputMax = cmd.OutputMax; + this.HalfWidth = FloatToUInt32Bits(cmd.HalfWidth); + this.LineCap = cmd.LineCap; + this.LineJoin = cmd.LineJoin; + this.MiterLimit = FloatToUInt32Bits(cmd.MiterLimit); + } + } + + /// + /// GPU-side stroke expand config matching the WGSL StrokeExpandConfig struct layout. + /// + [StructLayout(LayoutKind.Sequential)] + private readonly struct GpuStrokeExpandConfig + { + public readonly uint TotalInputEdges; + public readonly uint CommandCount; + + public GpuStrokeExpandConfig(uint totalInputEdges, uint commandCount) + { + this.TotalInputEdges = totalInputEdges; + this.CommandCount = commandCount; + } + } + + /// + /// Contains stroke expansion data needed for the GPU dispatch. + /// + private readonly struct StrokeExpandInfo + { + public readonly List? Commands; + public readonly int TotalCenterlineEdges; + + public StrokeExpandInfo(List? commands, int totalCenterlineEdges) + { + this.Commands = commands; + this.TotalCenterlineEdges = totalCenterlineEdges; + } + + public bool HasCommands => this.Commands is not null && this.Commands.Count > 0; + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 723b57b86..e7f897d32 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -41,6 +41,10 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp private const uint PreparedBrushTypeImage = 1; private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; + private const string StrokeExpandPipelineKey = "stroke-expand"; + private const string StrokeExpandCommandsBufferKey = "stroke-expand/commands"; + private const string StrokeExpandConfigBufferKey = "stroke-expand/config"; + private const string StrokeExpandCounterBufferKey = "stroke-expand/counter"; private const int UniformBufferOffsetAlignment = 256; private const int CallbackTimeoutMilliseconds = 10_000; @@ -555,11 +559,27 @@ private bool TryRenderPreparedFlush( out int totalBandOffsetEntries, out WgpuBuffer* bandOffsetsBuffer, out nuint bandOffsetsBufferSize, + out StrokeExpandInfo strokeExpandInfo, out error)) { return false; } + // Dispatch stroke expansion before composite rasterization. + // This generates outline edges from centerline edges in a separate compute pass. + if (strokeExpandInfo.HasCommands) + { + if (!this.TryDispatchStrokeExpand( + flushContext, + edgeBuffer, + edgeBufferSize, + strokeExpandInfo, + out error)) + { + return false; + } + } + if (!this.TryDispatchPreparedCompositeCommands( flushContext, backdropTextureView, @@ -681,7 +701,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out try { int flushCommandCount = commandCount; - Span parameters = parametersOwner.Memory.Span[..commandCount]; + Span parameters = parametersOwner.Memory.Span; TextureView* brushTextureView = backdropTextureView; nint brushTextureViewHandle = (nint)backdropTextureView; bool hasImageTexture = false; @@ -790,12 +810,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out command.GraphicsOptions.BlendPercentage, solidColor, command.GraphicsOptions.Antialias ? 0u : 1u, - command.GraphicsOptions.AntialiasThreshold, - edgePlacement.IsStroke ? 1u : 0u, - edgePlacement.StrokeWidth * 0.5f, - edgePlacement.StrokeLineCap, - edgePlacement.StrokeLineJoin, - edgePlacement.StrokeMiterLimit); + command.GraphicsOptions.AntialiasThreshold); parameters[commandIndex] = commandParameters; commandIndex++; @@ -955,6 +970,254 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out return true; } + /// + /// Dispatches the stroke expand compute shader to generate outline edges + /// from centerline edges. Must be called before the composite dispatch + /// so the generated edges are available for the fill rasterizer. + /// + private bool TryDispatchStrokeExpand( + WebGPUFlushContext flushContext, + WgpuBuffer* edgeBuffer, + nuint edgeBufferSize, + StrokeExpandInfo expandInfo, + out string? error) + { + error = null; + if (!expandInfo.HasCommands) + { + return true; + } + + List commands = expandInfo.Commands!; + + // Create or get the pipeline. + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + => TryCreateStrokeExpandBindGroupLayout(api, device, out layout, out layoutError); + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + StrokeExpandPipelineKey, + StrokeExpandComputeShader.Code, + LayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + // Build GPU command array. + int commandCount = commands.Count; + using IMemoryOwner gpuCommandsOwner = flushContext.MemoryAllocator.Allocate(commandCount); + Span gpuCommands = gpuCommandsOwner.Memory.Span; + for (int i = 0; i < commandCount; i++) + { + gpuCommands[i] = new GpuStrokeExpandCommand(commands[i]); + } + + nuint commandsSize = (nuint)(commandCount * Unsafe.SizeOf()); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + StrokeExpandCommandsBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + commandsSize, + out WgpuBuffer* commandsBuffer, + out _, + out error)) + { + return false; + } + + fixed (GpuStrokeExpandCommand* commandsPtr = &MemoryMarshal.GetReference(gpuCommands)) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + commandsBuffer, + 0, + commandsPtr, + commandsSize); + } + + // Config uniform. + nuint configSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + StrokeExpandConfigBufferKey, + BufferUsage.Uniform | BufferUsage.CopyDst, + configSize, + out WgpuBuffer* configBuffer, + out _, + out error)) + { + return false; + } + + GpuStrokeExpandConfig config = new( + (uint)expandInfo.TotalCenterlineEdges, + (uint)commandCount); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + configBuffer, + 0, + &config, + configSize); + + // Atomic output counters — one u32 per command, initialized to 0. + nuint counterSize = (nuint)(expandInfo.Commands!.Count * sizeof(uint)); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + StrokeExpandCounterBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + counterSize, + out WgpuBuffer* counterBuffer, + out _, + out error)) + { + return false; + } + + // Clear the counter to 0. + flushContext.Api.CommandEncoderClearBuffer(flushContext.CommandEncoder, counterBuffer, 0, counterSize); + + // Bind group. + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[4]; + bindGroupEntries[0] = new BindGroupEntry + { + Binding = 0, + Buffer = edgeBuffer, + Offset = 0, + Size = edgeBufferSize + }; + bindGroupEntries[1] = new BindGroupEntry + { + Binding = 1, + Buffer = commandsBuffer, + Offset = 0, + Size = commandsSize + }; + bindGroupEntries[2] = new BindGroupEntry + { + Binding = 2, + Buffer = configBuffer, + Offset = 0, + Size = configSize + }; + bindGroupEntries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = counterBuffer, + Offset = 0, + Size = counterSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 4, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + error = "Failed to create stroke expand bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + + // Dispatch in a separate compute pass (guarantees ordering before composite pass). + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass( + flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + error = "Failed to begin stroke expand compute pass."; + return false; + } + + try + { + uint workgroupCount = DivideRoundUp(expandInfo.TotalCenterlineEdges, 256); + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, workgroupCount, 1, 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + return true; + } + + private static bool TryCreateStrokeExpandBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + layout = null; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Storage, + HasDynamicOffset = false, + MinBindingSize = sizeof(uint) + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create stroke expand bind group layout."; + return false; + } + + error = null; + return true; + } + private static bool TryGetOrCreateImageTextureView( WebGPUFlushContext flushContext, Image image, @@ -1735,23 +1998,13 @@ public EdgePlacement( uint edgeCount, uint fillRule, uint csrOffsetsStart, - uint csrBandCount, - bool isStroke = false, - float strokeWidth = 0f, - uint strokeLineCap = 0, - uint strokeLineJoin = 0, - float strokeMiterLimit = 0f) + uint csrBandCount) { this.EdgeStart = edgeStart; this.EdgeCount = edgeCount; this.FillRule = fillRule; this.CsrOffsetsStart = csrOffsetsStart; this.CsrBandCount = csrBandCount; - this.IsStroke = isStroke; - this.StrokeWidth = strokeWidth; - this.StrokeLineCap = strokeLineCap; - this.StrokeLineJoin = strokeLineJoin; - this.StrokeMiterLimit = strokeMiterLimit; } public uint EdgeStart { get; } @@ -1763,16 +2016,6 @@ public EdgePlacement( public uint CsrOffsetsStart { get; } public uint CsrBandCount { get; } - - public bool IsStroke { get; } - - public float StrokeWidth { get; } - - public uint StrokeLineCap { get; } - - public uint StrokeLineJoin { get; } - - public float StrokeMiterLimit { get; } } /// @@ -1868,12 +2111,8 @@ private readonly struct PreparedCompositeParameters public readonly uint SolidA; public readonly uint RasterizationMode; public readonly uint AntialiasThreshold; - public readonly uint StrokeMode; - public readonly uint StrokeHalfWidth; - public readonly uint StrokeLineCap; - public readonly uint StrokeLineJoin; - public readonly uint StrokeMiterLimit; - public readonly uint StrokePad0; + public readonly uint Pad0; + public readonly uint Pad1; public PreparedCompositeParameters( int destinationX, @@ -1898,12 +2137,7 @@ public PreparedCompositeParameters( float blendPercentage, Vector4 solidColor, uint rasterizationMode, - float antialiasThreshold, - uint strokeMode = 0, - float strokeHalfWidth = 0f, - uint strokeLineCap = 0, - uint strokeLineJoin = 0, - float strokeMiterLimit = 0f) + float antialiasThreshold) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -1931,12 +2165,8 @@ public PreparedCompositeParameters( this.SolidA = FloatToUInt32Bits(solidColor.W); this.RasterizationMode = rasterizationMode; this.AntialiasThreshold = FloatToUInt32Bits(antialiasThreshold); - this.StrokeMode = strokeMode; - this.StrokeHalfWidth = FloatToUInt32Bits(strokeHalfWidth); - this.StrokeLineCap = strokeLineCap; - this.StrokeLineJoin = strokeLineJoin; - this.StrokeMiterLimit = FloatToUInt32Bits(strokeMiterLimit); - this.StrokePad0 = 0; + this.Pad0 = 0; + this.Pad1 = 0; } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 2c780df2a..57d9c56ef 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -790,9 +790,8 @@ internal static void UploadTextureFromRegion( int alignedRowBytesInt = checked((int)alignedRowBytes); int packedByteCount = checked(alignedRowBytesInt * sourceRegion.Height); - using IMemoryOwner packedOwner = memoryAllocator.Allocate(packedByteCount); - Span packedData = packedOwner.Memory.Span[..packedByteCount]; - packedData.Clear(); + using IMemoryOwner packedOwner = memoryAllocator.Allocate(packedByteCount, AllocationOptions.Clean); + Span packedData = packedOwner.Memory.Span; for (int y = 0; y < sourceRegion.Height; y++) { ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index 9822ba4ea..0a4dd9138 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -27,7 +27,7 @@ public InProcessConfig() this.AddJob( Job.Default .WithLaunchCount(3) - .WithWarmupCount(15) + .WithWarmupCount(40) .WithIterationCount(40) .WithToolchain(InProcessEmitToolchain.Instance)); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index cb338de92..347eb7c7a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -435,13 +435,11 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - // Stroking difference are minor subpixel differences but accumulate more than typical rasterization differences, - // so use a higher threshold here and below. - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.0292F); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.007F); Rectangle textRegion = Rectangle.Intersect( new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); - AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.0376F); + AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.009F); } [Theory] @@ -1073,7 +1071,120 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider LineJoinValues { get; } = new() + { + LineJoin.Miter, + LineJoin.MiterRevert, + LineJoin.MiterRound, + LineJoin.Bevel, + LineJoin.Round + }; + + [Theory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Miter)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.MiterRevert)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.MiterRound)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Bevel)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Round)] + public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvider provider, LineJoin lineJoin) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Sharp angles to exercise join behavior. + PathBuilder pb = new(); + pb.AddLine(new PointF(30, 250), new PointF(100, 30)); + pb.AddLine(new PointF(100, 30), new PointF(170, 250)); + pb.AddLine(new PointF(170, 250), new PointF(240, 30)); + pb.AddLine(new PointF(240, 30), new PointF(370, 150)); + IPath path = pb.Build(); + + Pen pen = new SolidPen(new PenOptions(Color.DarkBlue, 12F) + { + StrokeOptions = new StrokeOptions { LineJoin = lineJoin } + }); + + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); + } + + [Theory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Butt)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Square)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Round)] + public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvider provider, LineCap lineCap) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Open path to exercise cap behavior at endpoints. + PathBuilder pb = new(); + pb.AddLine(new PointF(50, 150), new PointF(200, 50)); + pb.AddLine(new PointF(200, 50), new PointF(350, 150)); + IPath path = pb.Build(); + + Pen pen = new SolidPen(new PenOptions(Color.DarkBlue, 16F) + { + StrokeOptions = new StrokeOptions { LineCap = lineCap } + }); + + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTripletNoRef(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); } [Theory] diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png new file mode 100644 index 000000000..98e4f91a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:408c8ac372fd34911814c3f4500542c47f1ddce3d88c7217ae248438c34a5026 +size 858 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png new file mode 100644 index 000000000..b1654d308 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92cb29d99646955e70bcd07b0061471c3168dd471f6d0280bebfbb48f1b5dedc +size 957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b1654d308 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92cb29d99646955e70bcd07b0061471c3168dd471f6d0280bebfbb48f1b5dedc +size 957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png new file mode 100644 index 000000000..bca146111 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db9d2722cb695c4913c07ef49fd1ea38d5567493f676982c9605b4021516a530 +size 996 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png new file mode 100644 index 000000000..09b1672a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:420a03c8f1f59cba44111ece194b71958d2d519228e1c26427e02cca87173fd0 +size 1083 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png new file mode 100644 index 000000000..09b1672a6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:420a03c8f1f59cba44111ece194b71958d2d519228e1c26427e02cca87173fd0 +size 1083 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png new file mode 100644 index 000000000..bf0d01419 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eb139670ddcae46d9e57107bf5f2e5e79befcbbafbdc61d9a5998f06c6c82da +size 837 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png new file mode 100644 index 000000000..2cc18734a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8f2674e83b193e6b9957ed1c9ce07bd597a0b9d6e06451545a108a0d0908173 +size 2361 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png new file mode 100644 index 000000000..2cc18734a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8f2674e83b193e6b9957ed1c9ce07bd597a0b9d6e06451545a108a0d0908173 +size 2361 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png new file mode 100644 index 000000000..d0817de1e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e71fa6672efffc6bc3a98b5fc18c6b8a01c93025183be6e62d85ac725470d39a +size 3000 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png new file mode 100644 index 000000000..9077c5320 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77fec86277b861b6909c6d1ae53903fe342d578ee19287763b9df264e596ed70 +size 7692 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9077c5320 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77fec86277b861b6909c6d1ae53903fe342d578ee19287763b9df264e596ed70 +size 7692 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png new file mode 100644 index 000000000..2607856b9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b +size 3141 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png new file mode 100644 index 000000000..2607856b9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b +size 3141 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png new file mode 100644 index 000000000..2607856b9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b +size 3141 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3e34931c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c +size 8181 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png new file mode 100644 index 000000000..3756914ad --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28272eb411bf50b7903999be32121f6c498598899af3dd6517d5aeefb0b3371f +size 3095 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png new file mode 100644 index 000000000..e656f164b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28a8b235e21f19b9b2dd35f666fec1f21c31da9c5b84732ad1c9243fc78c5cc +size 3419 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png new file mode 100644 index 000000000..e656f164b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28a8b235e21f19b9b2dd35f666fec1f21c31da9c5b84732ad1c9243fc78c5cc +size 3419 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png index 2265c505f..f0500c2d7 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d8d4ddb806acf510c308d2644b316006becefd9f07aed36b4bd8b8d850bfa41 -size 1816 +oid sha256:ac7a7ae06eb322c60aa1faad8c880df71e943ae37948ccf50dd2394a044012f3 +size 2264 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png index 2265c505f..f0500c2d7 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d8d4ddb806acf510c308d2644b316006becefd9f07aed36b4bd8b8d850bfa41 -size 1816 +oid sha256:ac7a7ae06eb322c60aa1faad8c880df71e943ae37948ccf50dd2394a044012f3 +size 2264 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png index 283285402..9200bf044 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fec9ff20f83a4f537e5a743094cc1edb263676fa173cf6790481ae76ecb81890 -size 34079 +oid sha256:c2e29507a7e569a2f0a887f196719b5943495184d9b33c8319760bcc78a9a0a7 +size 36329 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png index 283285402..9200bf044 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fec9ff20f83a4f537e5a743094cc1edb263676fa173cf6790481ae76ecb81890 -size 34079 +oid sha256:c2e29507a7e569a2f0a887f196719b5943495184d9b33c8319760bcc78a9a0a7 +size 36329 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png index e56076d85..4a8a5c6e5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89b70c1701bb62b2d879e595555bf5c19f973ff9ec15f2b1ffcf3029aa96fcda -size 12204 +oid sha256:c3154bc52e2942fef0578166087ef4a7cd2b8d3bbff3a9eb2a75ca216143e860 +size 12952 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png index e56076d85..4a8a5c6e5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89b70c1701bb62b2d879e595555bf5c19f973ff9ec15f2b1ffcf3029aa96fcda -size 12204 +oid sha256:c3154bc52e2942fef0578166087ef4a7cd2b8d3bbff3a9eb2a75ca216143e860 +size 12952 From bc0fb83dff5ce7e3b8bd907a36b030934d6b06fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 15:04:36 +1000 Subject: [PATCH 108/136] Add per-band stroke rasterization and StrokeEdgeFlags --- samples/WebGPUWindowDemo/Program.cs | 4 +- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 13 +- .../Processing/Backends}/DashPathSplitter.cs | 2 +- .../Backends/DefaultDrawingBackend.cs | 65 +- .../Processing/Backends/DefaultRasterizer.cs | 1135 ++++++++++++++++- .../Processing/Backends/StrokeEdgeFlags.cs | 36 + 6 files changed, 1199 insertions(+), 56 deletions(-) rename src/{ImageSharp.Drawing.WebGPU => ImageSharp.Drawing/Processing/Backends}/DashPathSplitter.cs (99%) create mode 100644 src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index 371640c83..db75209c5 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -155,9 +155,7 @@ private static void OnLoad() Console.WriteLine($"Device: 0x{(nuint)device:X}, Queue: 0x{(nuint)queue:X}"); - // Query surface capabilities and configure the swap chain. - wgpu.SurfaceGetCapabilities(surface, adapter, ref surfaceCapabilities); - Console.WriteLine($"Surface format: {surfaceCapabilities.Formats[0]}"); + // Configure the swap chain. ConfigureSwapchain(); // Initialize the ImageSharp.Drawing WebGPU backend and attach it to a diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 2df64d1c5..6d59b46ce 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -823,7 +823,7 @@ private static bool TryBuildStrokeEdges( Y0 = vy, X1 = (int)MathF.Round(((pv.X - interestX) + samplingOffsetX) * FixedOne), Y1 = (int)MathF.Round(((pv.Y - interestY) + samplingOffsetY) * FixedOne), - Flags = 32, // EDGE_JOIN + Flags = StrokeEdgeFlags.Join, AdjX = (int)MathF.Round(((nv.X - interestX) + samplingOffsetX) * FixedOne), AdjY = (int)MathF.Round(((nv.Y - interestY) + samplingOffsetY) * FixedOne), }); @@ -844,7 +844,7 @@ private static bool TryBuildStrokeEdges( Y0 = csy, X1 = (int)MathF.Round(((adjStart.X - interestX) + samplingOffsetX) * FixedOne), Y1 = (int)MathF.Round(((adjStart.Y - interestY) + samplingOffsetY) * FixedOne), - Flags = 64, // EDGE_CAP_START + Flags = StrokeEdgeFlags.CapStart, }); edgeYRanges.Add((csy - yExpansionFixed, csy + yExpansionFixed)); @@ -859,7 +859,7 @@ private static bool TryBuildStrokeEdges( Y0 = cey, X1 = (int)MathF.Round(((adjEnd.X - interestX) + samplingOffsetX) * FixedOne), Y1 = (int)MathF.Round(((adjEnd.Y - interestY) + samplingOffsetY) * FixedOne), - Flags = 128, // EDGE_CAP_END + Flags = StrokeEdgeFlags.CapEnd, }); edgeYRanges.Add((cey - yExpansionFixed, cey + yExpansionFixed)); @@ -1191,12 +1191,9 @@ private struct GpuEdge public int Y1; /// - /// Bit flags for stroke edge metadata. - /// Bit 0: open start — the (X0,Y0) endpoint is an open path start (cap applies). - /// Bit 1: open end — the (X1,Y1) endpoint is an open path end (cap applies). - /// Bit 2: bevel fill — this edge is a bevel fill chord (AdjX,AdjY = join vertex). + /// Stroke edge type flags matching the WGSL shader constants. /// - public int Flags; + public StrokeEdgeFlags Flags; /// /// Auxiliary coordinates (fixed-point). For bevel fill edges, stores the diff --git a/src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs b/src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs similarity index 99% rename from src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs rename to src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs index f12f873f5..467d9aac0 100644 --- a/src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// Splits a path into dash segments without performing stroke expansion. /// Each "on" dash segment is returned as an open sub-path. /// -internal static class DashPathSplitter +public static class DashPathSplitter { /// /// Splits the given path into dash segments based on the provided pattern. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 80f2a9937..0c96dd5fd 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -155,26 +155,30 @@ private void FlushPreparedBatch( CompositionCoverageDefinition definition = compositionBatch.Definition; - // When the definition carries stroke metadata, expand the centerline - // path into a filled outline before rasterization. IPath rasterPath = definition.Path; RasterizerOptions rasterizerOptions = definition.RasterizerOptions; - if (definition.IsStroke) + if (definition.IsStroke && definition.StrokePattern.Length > 0) { - rasterPath = definition.StrokePattern.Length > 0 - ? rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokePattern.Span, definition.StrokeOptions!) - : rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokeOptions!); - - // Compute the exact interest from the actual stroke outline bounds - // so band boundaries and coverage values match the old canvas-side path. - RectangleF outlineBounds = rasterPath.Bounds; - outlineBounds = new RectangleF(outlineBounds.X + 0.5F, outlineBounds.Y + 0.5F, outlineBounds.Width, outlineBounds.Height); + // Dashed strokes: split into dash segments on the CPU, then stroke-expand + // each segment via the per-band parallel path (same as solid strokes). + rasterPath = DashPathSplitter.SplitDashes(rasterPath, definition.StrokeWidth, definition.StrokePattern.Span); + + // Recompute interest from the split path bounds with stroke expansion. + float halfWidth = definition.StrokeWidth * 0.5f; + float maxExtent = halfWidth * Math.Max((float)(definition.StrokeOptions?.MiterLimit ?? 4.0), 1.0f); + RectangleF pathBounds = rasterPath.Bounds; + pathBounds = new RectangleF( + pathBounds.X + 0.5F - maxExtent, + pathBounds.Y + 0.5F - maxExtent, + pathBounds.Width + (maxExtent * 2), + pathBounds.Height + (maxExtent * 2)); + Rectangle interest = Rectangle.FromLTRB( - (int)MathF.Floor(outlineBounds.Left), - (int)MathF.Floor(outlineBounds.Top), - (int)MathF.Ceiling(outlineBounds.Right), - (int)MathF.Ceiling(outlineBounds.Bottom)); + (int)MathF.Floor(pathBounds.Left), + (int)MathF.Floor(pathBounds.Top), + (int)MathF.Ceiling(pathBounds.Right), + (int)MathF.Ceiling(pathBounds.Bottom)); rasterizerOptions = new RasterizerOptions( interest, @@ -183,7 +187,7 @@ private void FlushPreparedBatch( rasterizerOptions.SamplingOrigin, rasterizerOptions.AntialiasThreshold); - // Re-prepare commands with the actual outline interest so destination + // Re-prepare commands with the dash-split interest so destination // regions and source offsets are aligned with the rasterizer. CompositionScenePlanner.ReprepareBatchCommands(compositionBatch.Commands, target.Bounds, interest); } @@ -213,12 +217,29 @@ private void FlushPreparedBatch( destinationBounds, rasterizerOptions.Interest.Top); - DefaultRasterizer.RasterizeRows( - rasterPath, - rasterizerOptions, - configuration.MemoryAllocator, - operation.InvokeCoverageRow, - ref reusableScratch); + if (definition.IsStroke) + { + // All strokes (solid and dashed) use per-band parallel stroke expansion. + DefaultRasterizer.RasterizeStrokeRows( + rasterPath, + rasterizerOptions, + configuration.MemoryAllocator, + operation.InvokeCoverageRow, + ref reusableScratch, + definition.StrokeWidth, + definition.StrokeOptions!.LineJoin, + definition.StrokeOptions!.LineCap, + (float)definition.StrokeOptions!.MiterLimit); + } + else + { + DefaultRasterizer.RasterizeRows( + rasterPath, + rasterizerOptions, + configuration.MemoryAllocator, + operation.InvokeCoverageRow, + ref reusableScratch); + } } finally { diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 2b99e7b64..4126a4bbc 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -112,6 +112,53 @@ public static void RasterizeRowsSequential( } } + /// + /// Rasterizes a stroke path by expanding centerline edges into outline edges per-band in parallel. + /// + /// Centerline path to stroke. + /// Rasterization options (interest rect should already include stroke expansion). + /// Temporary buffer allocator. + /// Coverage row callback invoked once per emitted row. + /// Stroke width in pixels. + /// Outer join style. + /// Cap style for open contour endpoints. + /// Miter limit for miter-family joins. + public static void RasterizeStrokeRows( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + float strokeWidth, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + WorkerScratch? scratch = null; + try + { + RasterizeStrokeCoreRows(path, options, allocator, rowHandler, allowParallel: true, ref scratch, strokeWidth, lineJoin, lineCap, miterLimit); + } + finally + { + scratch?.Dispose(); + } + } + + /// + /// Rasterizes a stroke path with caller-managed scratch reuse. + /// + internal static void RasterizeStrokeRows( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch, + float strokeWidth, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + => RasterizeStrokeCoreRows(path, options, allocator, rowHandler, allowParallel: true, ref reusableScratch, strokeWidth, lineJoin, lineCap, miterLimit); + /// /// Shared entry point for trimmed-row rasterization. /// @@ -206,6 +253,128 @@ private static void RasterizeCoreRows( ref reusableScratch); } + /// + /// Shared entry point for stroke-aware trimmed-row rasterization. + /// Expands centerline edges into outline edges per-band and rasterizes directly. + /// + private static void RasterizeStrokeCoreRows( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + bool allowParallel, + ref WorkerScratch? reusableScratch, + float strokeWidth, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + Rectangle interest = options.Interest; + int width = interest.Width; + int height = interest.Height; + if (width <= 0 || height <= 0) + { + return; + } + + int wordsPerRow = BitVectorsForMaxBitCount(width); + int maxBandRows = 0; + long coverStride = (long)width * 2; + if (coverStride > int.MaxValue || !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) + { + ThrowInterestBoundsTooLarge(); + } + + int coverStrideInt = (int)coverStride; + bool samplePixelCenter = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + + // Flatten path and collect contour info, preserving open/closed state for cap/join generation. + List contours = []; + int totalVertexCount = 0; + foreach (ISimplePath sp in path.Flatten()) + { + if (sp.Points.Length < 2) + { + continue; + } + + contours.Add(sp); + totalVertexCount += sp.Points.Length; + } + + if (totalVertexCount == 0) + { + return; + } + + // Max stroke descriptors: closed contours emit 2*N (sides + joins), + // open contours emit 2*N-1 (sides + interior joins + 2 caps). + int maxEdgeCount = totalVertexCount * 2; + using IMemoryOwner edgeDataOwner = allocator.Allocate(maxEdgeCount); + int edgeCount = BuildStrokeEdgeTable( + contours, + interest.Left, + interest.Top, + samplingOffsetX, + samplingOffsetY, + edgeDataOwner.Memory.Span); + + if (edgeCount <= 0) + { + return; + } + + float halfWidth = strokeWidth / 2f; + float expansion = halfWidth * MathF.Max(miterLimit, 1f); + + if (allowParallel && + TryRasterizeStrokeParallel( + edgeDataOwner.Memory, + edgeCount, + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + allocator, + rowHandler, + ref reusableScratch, + halfWidth, + expansion, + lineJoin, + lineCap, + miterLimit)) + { + return; + } + + RasterizeStrokeSequentialBands( + edgeDataOwner.Memory.Span[..edgeCount], + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + allocator, + rowHandler, + ref reusableScratch, + halfWidth, + expansion, + lineJoin, + lineCap, + miterLimit); + } + /// /// Sequential implementation using band buckets over the prebuilt edge table. /// @@ -476,23 +645,324 @@ private static void RasterizeSingleTileDirect( reusableScratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); } - WorkerScratch scratch = reusableScratch; - Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode, antialiasThreshold); - context.RasterizeEdgeTable(edges, bandTop: 0); - context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); - context.ResetTouchedRows(); + WorkerScratch scratch = reusableScratch; + Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode, antialiasThreshold); + context.RasterizeEdgeTable(edges, bandTop: 0); + context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } + + /// + /// Sequential stroke rasterization using band buckets over the prebuilt stroke edge table. + /// + private static void RasterizeStrokeSequentialBands( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStrideInt, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch, + float halfWidth, + float expansion, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + int bandHeight = maxBandRows; + int bandCount = (height + bandHeight - 1) / bandHeight; + if (bandCount < 1) + { + return; + } + + if (!TryBuildBandSortedStrokeEdges( + edges, + bandCount, + bandHeight, + expansion, + allocator, + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner bandOffsetsOwner)) + { + ThrowInterestBoundsTooLarge(); + } + + using (sortedEdgesOwner) + using (bandOffsetsOwner) + { + Span sortedEdges = sortedEdgesOwner.Memory.Span; + Span bandOffsets = bandOffsetsOwner.Memory.Span; + + if (reusableScratch == null || !reusableScratch.CanReuse(wordsPerRow, coverStrideInt, width, bandHeight)) + { + reusableScratch?.Dispose(); + reusableScratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); + } + + WorkerScratch scratch = reusableScratch; + for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) + { + int bandTop = bandIndex * bandHeight; + int currentBandHeight = Math.Min(bandHeight, height - bandTop); + int start = bandOffsets[bandIndex]; + int length = bandOffsets[bandIndex + 1] - start; + if (length == 0) + { + continue; + } + + Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode, antialiasThreshold); + context.RasterizeStrokeEdges(sortedEdges.Slice(start, length), bandTop, halfWidth, lineJoin, lineCap, miterLimit); + context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } + } + } + + /// + /// Attempts to execute the tiled parallel stroke scanner. + /// + private static bool TryRasterizeStrokeParallel( + Memory edgeMemory, + int edgeCount, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch, + float halfWidth, + float expansion, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); + if (tileHeight < 1) + { + return false; + } + + int tileCount = (height + tileHeight - 1) / tileHeight; + if (tileCount == 1 || edgeCount <= 64) + { + RasterizeStrokeSingleTileDirect( + edgeMemory.Span[..edgeCount], + width, + height, + interestTop, + wordsPerRow, + coverStride, + intersectionRule, + rasterizationMode, + antialiasThreshold, + allocator, + rowHandler, + ref reusableScratch, + halfWidth, + lineJoin, + lineCap, + miterLimit); + + return true; + } + + if (Environment.ProcessorCount < 2) + { + return false; + } + + if (!TryBuildBandSortedStrokeEdges( + edgeMemory.Span[..edgeCount], + tileCount, + tileHeight, + expansion, + allocator, + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner tileOffsetsOwner)) + { + return false; + } + + using (sortedEdgesOwner) + using (tileOffsetsOwner) + { + Memory sortedEdgesMemory = sortedEdgesOwner.Memory; + Memory tileOffsetsMemory = tileOffsetsOwner.Memory; + + ParallelOptions parallelOptions = new() + { + MaxDegreeOfParallelism = Math.Min(MaxParallelWorkerCount, Math.Min(Environment.ProcessorCount, tileCount)) + }; + + _ = Parallel.For( + 0, + tileCount, + parallelOptions, + () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), + (tileIndex, _, worker) => + { + Context context = default; + bool hasCoverage = false; + int bandTop = tileIndex * tileHeight; + try + { + Span tileOffsets = tileOffsetsMemory.Span; + int bandHeight = Math.Min(tileHeight, height - bandTop); + int start = tileOffsets[tileIndex]; + int length = tileOffsets[tileIndex + 1] - start; + if (length > 0) + { + ReadOnlySpan tileEdges = sortedEdgesMemory.Span.Slice(start, length); + context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode, antialiasThreshold); + context.RasterizeStrokeEdges(tileEdges, bandTop, halfWidth, lineJoin, lineCap, miterLimit); + hasCoverage = true; + context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler); + } + } + finally + { + if (hasCoverage) + { + context.ResetTouchedRows(); + } + } + + return worker; + }, + static worker => worker.Dispose()); + + return true; + } + } + + /// + /// Rasterizes stroke edges in a single tile directly into the caller callback. + /// + private static void RasterizeStrokeSingleTileDirect( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold, + MemoryAllocator allocator, + RasterizerCoverageRowHandler rowHandler, + ref WorkerScratch? reusableScratch, + float halfWidth, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + if (reusableScratch == null || !reusableScratch.CanReuse(wordsPerRow, coverStride, width, height)) + { + reusableScratch?.Dispose(); + reusableScratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); + } + + WorkerScratch scratch = reusableScratch; + Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode, antialiasThreshold); + context.RasterizeStrokeEdges(edges, bandTop: 0, halfWidth, lineJoin, lineCap, miterLimit); + context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler); + context.ResetTouchedRows(); + } + + /// + /// Builds a band-sorted edge buffer where edges are duplicated into each band they touch. + /// Band offsets provide direct indexing — no per-band edge index array is needed. + /// + private static bool TryBuildBandSortedEdges( + ReadOnlySpan edges, + int bucketCount, + int bucketHeight, + MemoryAllocator allocator, + out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner offsetsOwner) + { + using IMemoryOwner countsOwner = allocator.Allocate(bucketCount, AllocationOptions.Clean); + Span counts = countsOwner.Memory.Span; + long totalRefs = 0; + for (int i = 0; i < edges.Length; i++) + { + ref readonly EdgeData edge = ref edges[i]; + int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; + int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; + int startBucket = minRow / bucketHeight; + int endBucket = maxRow / bucketHeight; + totalRefs += (endBucket - startBucket) + 1; + if (totalRefs > int.MaxValue) + { + sortedEdgesOwner = null!; + offsetsOwner = null!; + return false; + } + + for (int b = startBucket; b <= endBucket; b++) + { + counts[b]++; + } + } + + int totalEdges = (int)totalRefs; + offsetsOwner = allocator.Allocate(bucketCount + 1); + Span offsets = offsetsOwner.Memory.Span; + int offset = 0; + for (int b = 0; b < bucketCount; b++) + { + offsets[b] = offset; + offset += counts[b]; + } + + offsets[bucketCount] = offset; + using IMemoryOwner writeCursorOwner = allocator.Allocate(bucketCount); + Span writeCursor = writeCursorOwner.Memory.Span; + offsets[..bucketCount].CopyTo(writeCursor); + + sortedEdgesOwner = allocator.Allocate(totalEdges); + Span sorted = sortedEdgesOwner.Memory.Span; + for (int i = 0; i < edges.Length; i++) + { + ref readonly EdgeData edge = ref edges[i]; + int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; + int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; + int startBucket = minRow / bucketHeight; + int endBucket = maxRow / bucketHeight; + for (int b = startBucket; b <= endBucket; b++) + { + sorted[writeCursor[b]++] = edge; + } + } + + return true; } /// - /// Builds a band-sorted edge buffer where edges are duplicated into each band they touch. - /// Band offsets provide direct indexing — no per-band edge index array is needed. + /// Builds a band-sorted stroke edge buffer where descriptors are duplicated into each band + /// their stroke expansion could touch. /// - private static bool TryBuildBandSortedEdges( - ReadOnlySpan edges, + private static bool TryBuildBandSortedStrokeEdges( + ReadOnlySpan edges, int bucketCount, int bucketHeight, + float expansion, MemoryAllocator allocator, - out IMemoryOwner sortedEdgesOwner, + out IMemoryOwner sortedEdgesOwner, out IMemoryOwner offsetsOwner) { using IMemoryOwner countsOwner = allocator.Allocate(bucketCount, AllocationOptions.Clean); @@ -500,11 +970,29 @@ private static bool TryBuildBandSortedEdges( long totalRefs = 0; for (int i = 0; i < edges.Length; i++) { - ref readonly EdgeData edge = ref edges[i]; - int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; - int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; - int startBucket = minRow / bucketHeight; - int endBucket = maxRow / bucketHeight; + ref readonly StrokeEdgeData edge = ref edges[i]; + + // Side edges: outline extends halfWidth from both endpoints. + // Join/cap edges: geometry is centered on vertex (X0,Y0), extends by expansion. + float minY, maxY; + if (edge.Flags == StrokeEdgeFlags.None) + { + minY = MathF.Min(edge.Y0, edge.Y1) - expansion; + maxY = MathF.Max(edge.Y0, edge.Y1) + expansion; + } + else + { + minY = edge.Y0 - expansion; + maxY = edge.Y0 + expansion; + } + + int startBucket = Math.Max(0, (int)MathF.Floor(minY / bucketHeight)); + int endBucket = Math.Min(bucketCount - 1, (int)MathF.Floor(maxY / bucketHeight)); + if (startBucket > endBucket) + { + continue; + } + totalRefs += (endBucket - startBucket) + 1; if (totalRefs > int.MaxValue) { @@ -534,15 +1022,25 @@ private static bool TryBuildBandSortedEdges( Span writeCursor = writeCursorOwner.Memory.Span; offsets[..bucketCount].CopyTo(writeCursor); - sortedEdgesOwner = allocator.Allocate(totalEdges); - Span sorted = sortedEdgesOwner.Memory.Span; + sortedEdgesOwner = allocator.Allocate(Math.Max(totalEdges, 1)); + Span sorted = sortedEdgesOwner.Memory.Span; for (int i = 0; i < edges.Length; i++) { - ref readonly EdgeData edge = ref edges[i]; - int minRow = Math.Min(edge.Y0, edge.Y1) >> FixedShift; - int maxRow = (Math.Max(edge.Y0, edge.Y1) - 1) >> FixedShift; - int startBucket = minRow / bucketHeight; - int endBucket = maxRow / bucketHeight; + ref readonly StrokeEdgeData edge = ref edges[i]; + float minY, maxY; + if (edge.Flags == StrokeEdgeFlags.None) + { + minY = MathF.Min(edge.Y0, edge.Y1) - expansion; + maxY = MathF.Max(edge.Y0, edge.Y1) + expansion; + } + else + { + minY = edge.Y0 - expansion; + maxY = edge.Y0 + expansion; + } + + int startBucket = Math.Max(0, (int)MathF.Floor(minY / bucketHeight)); + int endBucket = Math.Min(bucketCount - 1, (int)MathF.Floor(maxY / bucketHeight)); for (int b = startBucket; b <= endBucket; b++) { sorted[writeCursor[b]++] = edge; @@ -552,6 +1050,117 @@ private static bool TryBuildBandSortedEdges( return true; } + /// + /// Builds a stroke edge table from flattened path contours. + /// Each contour produces side edge descriptors for segments, join descriptors at interior + /// vertices, and cap descriptors at endpoints of open contours. + /// + /// Flattened path contours with open/closed state. + /// Interest left in absolute coordinates. + /// Interest top in absolute coordinates. + /// Horizontal sampling offset. + /// Vertical sampling offset. + /// Destination span for stroke edge descriptors. + /// Number of valid descriptors written. + private static int BuildStrokeEdgeTable( + List contours, + float minX, + float minY, + float samplingOffsetX, + float samplingOffsetY, + Span destination) + { + int count = 0; + for (int c = 0; c < contours.Count; c++) + { + ISimplePath sp = contours[c]; + ReadOnlySpan pts = sp.Points.Span; + int n = pts.Length; + if (n < 2) + { + continue; + } + + bool isClosed = sp.IsClosed; + float offX = samplingOffsetX - minX; + float offY = samplingOffsetY - minY; + + if (isClosed) + { + // Side edges for all segments, including closing segment back to first vertex. + for (int i = 0; i < n; i++) + { + int j = (i + 1) % n; + destination[count++] = new StrokeEdgeData( + pts[i].X + offX, + pts[i].Y + offY, + pts[j].X + offX, + pts[j].Y + offY, + 0); + } + + // Join at each vertex. + for (int i = 0; i < n; i++) + { + int prev = (i - 1 + n) % n; + int next = (i + 1) % n; + destination[count++] = new StrokeEdgeData( + pts[i].X + offX, + pts[i].Y + offY, + pts[prev].X + offX, + pts[prev].Y + offY, + StrokeEdgeFlags.Join, + pts[next].X + offX, + pts[next].Y + offY); + } + } + else + { + // Side edges for all segments. + for (int i = 0; i < n - 1; i++) + { + destination[count++] = new StrokeEdgeData( + pts[i].X + offX, + pts[i].Y + offY, + pts[i + 1].X + offX, + pts[i + 1].Y + offY, + 0); + } + + // Interior joins. + for (int i = 1; i < n - 1; i++) + { + destination[count++] = new StrokeEdgeData( + pts[i].X + offX, + pts[i].Y + offY, + pts[i - 1].X + offX, + pts[i - 1].Y + offY, + StrokeEdgeFlags.Join, + pts[i + 1].X + offX, + pts[i + 1].Y + offY); + } + + // Start cap. + destination[count++] = new StrokeEdgeData( + pts[0].X + offX, + pts[0].Y + offY, + pts[1].X + offX, + pts[1].Y + offY, + StrokeEdgeFlags.CapStart); + + // End cap. + destination[count++] = new StrokeEdgeData( + pts[n - 1].X + offX, + pts[n - 1].Y + offY, + pts[n - 2].X + offX, + pts[n - 2].Y + offY, + StrokeEdgeFlags.CapEnd); + } + } + + return count; + } + /// /// Builds an edge table in scanner-local coordinates. /// @@ -1013,6 +1622,447 @@ public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) } } + /// + /// Expands stroke centerline edge descriptors into outline polygon edges and rasterizes them. + /// + /// Band-sorted stroke edge descriptors. + /// Top row of this context in global scanner-local coordinates. + /// Half the stroke width in pixels. + /// Outer join style. + /// Cap style for open contour endpoints. + /// Miter limit for miter-family joins. + public void RasterizeStrokeEdges( + ReadOnlySpan edges, + int bandTop, + float halfWidth, + LineJoin lineJoin, + LineCap lineCap, + float miterLimit) + { + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); + + for (int i = 0; i < edges.Length; i++) + { + ref readonly StrokeEdgeData edge = ref edges[i]; + StrokeEdgeFlags flags = edge.Flags; + + if (flags == StrokeEdgeFlags.None) + { + this.ExpandSideEdge(in edge, halfWidth, bandTopFixed, bandBottomFixed); + } + else if ((flags & StrokeEdgeFlags.Join) != 0) + { + this.ExpandJoinEdge(in edge, halfWidth, lineJoin, miterLimit, bandTopFixed, bandBottomFixed); + } + else if ((flags & StrokeEdgeFlags.CapStart) != 0) + { + this.ExpandCapEdge(in edge, halfWidth, lineCap, isStart: true, bandTopFixed, bandBottomFixed); + } + else + { + this.ExpandCapEdge(in edge, halfWidth, lineCap, isStart: false, bandTopFixed, bandBottomFixed); + } + } + } + + /// + /// Emits one outline edge into the rasterizer, converting from float to fixed-point + /// and clipping to band bounds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EmitOutlineEdge( + float ex0, + float ey0, + float ex1, + float ey1, + int bandTopFixed, + int bandBottomFixed) + { + int fy0 = FloatToFixed24Dot8(ey0); + int fy1 = FloatToFixed24Dot8(ey1); + if (fy0 == fy1) + { + return; + } + + int fx0 = FloatToFixed24Dot8(ex0); + int fx1 = FloatToFixed24Dot8(ex1); + + int minY = Math.Min(fy0, fy1); + int maxY = Math.Max(fy0, fy1); + if (minY >= bandBottomFixed || maxY <= bandTopFixed) + { + return; + } + + if (minY >= bandTopFixed && maxY <= bandBottomFixed) + { + this.RasterizeLine(fx0, fy0 - bandTopFixed, fx1, fy1 - bandTopFixed); + return; + } + + int x0 = fx0, y0 = fy0, x1 = fx1, y1 = fy1; + if (ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) + { + this.RasterizeLine(x0, y0 - bandTopFixed, x1, y1 - bandTopFixed); + } + } + + /// + /// Expands a side (segment) edge into two outline edges offset by the stroke normal. + /// + private void ExpandSideEdge( + in StrokeEdgeData edge, + float halfWidth, + int bandTopFixed, + int bandBottomFixed) + { + float dx = edge.X1 - edge.X0; + float dy = edge.Y1 - edge.Y0; + float len = MathF.Sqrt((dx * dx) + (dy * dy)); + if (len < 1e-6f) + { + return; + } + + float nx = (-dy / len) * halfWidth; + float ny = (dx / len) * halfWidth; + + // Left side. + this.EmitOutlineEdge( + edge.X0 + nx, + edge.Y0 + ny, + edge.X1 + nx, + edge.Y1 + ny, + bandTopFixed, + bandBottomFixed); + + // Right side (reversed winding). + this.EmitOutlineEdge( + edge.X1 - nx, + edge.Y1 - ny, + edge.X0 - nx, + edge.Y0 - ny, + bandTopFixed, + bandBottomFixed); + } + + /// + /// Expands a join descriptor into inner bevel + outer join edges. + /// Ported from the GPU StrokeExpandComputeShader join logic. + /// + private void ExpandJoinEdge( + in StrokeEdgeData edge, + float halfWidth, + LineJoin lineJoin, + float miterLimit, + int bandTopFixed, + int bandBottomFixed) + { + float vx = edge.X0, vy = edge.Y0; + float dx1 = vx - edge.X1, dy1 = vy - edge.Y1; + float len1 = MathF.Sqrt((dx1 * dx1) + (dy1 * dy1)); + if (len1 < 1e-6f) + { + return; + } + + float dx2 = edge.AdjX - vx, dy2 = edge.AdjY - vy; + float len2 = MathF.Sqrt((dx2 * dx2) + (dy2 * dy2)); + if (len2 < 1e-6f) + { + return; + } + + float nx1 = -dy1 / len1, ny1 = dx1 / len1; + float nx2 = -dy2 / len2, ny2 = dx2 / len2; + float cross = (dx1 * dy2) - (dy1 * dx2); + + float oax, oay, obx, oby, iax, iay, ibx, iby; + if (cross > 0) + { + oax = vx - (nx1 * halfWidth); + oay = vy - (ny1 * halfWidth); + obx = vx - (nx2 * halfWidth); + oby = vy - (ny2 * halfWidth); + iax = vx + (nx1 * halfWidth); + iay = vy + (ny1 * halfWidth); + ibx = vx + (nx2 * halfWidth); + iby = vy + (ny2 * halfWidth); + } + else + { + oax = vx + (nx1 * halfWidth); + oay = vy + (ny1 * halfWidth); + obx = vx + (nx2 * halfWidth); + oby = vy + (ny2 * halfWidth); + iax = vx - (nx1 * halfWidth); + iay = vy - (ny1 * halfWidth); + ibx = vx - (nx2 * halfWidth); + iby = vy - (ny2 * halfWidth); + } + + float ofx, ofy, otx, oty, ifx, ify, itx, ity; + if (cross > 0) + { + ofx = obx; + ofy = oby; + otx = oax; + oty = oay; + ifx = iax; + ify = iay; + itx = ibx; + ity = iby; + } + else + { + ofx = oax; + ofy = oay; + otx = obx; + oty = oby; + ifx = ibx; + ify = iby; + itx = iax; + ity = iay; + } + + // Inner join: always bevel. + this.EmitOutlineEdge(ifx, ify, itx, ity, bandTopFixed, bandBottomFixed); + + // Outer join. + bool miterHandled = false; + if (lineJoin is LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound) + { + float ux1 = dx1 / len1; + float uy1 = dy1 / len1; + float ux2 = dx2 / len2; + float uy2 = dy2 / len2; + float denom = (ux1 * uy2) - (uy1 * ux2); + if (MathF.Abs(denom) > 1e-4f) + { + float dpx = obx - oax; + float dpy = oby - oay; + float t = ((dpx * uy2) - (dpy * ux2)) / denom; + float mx = oax + (t * ux1); + float my = oay + (t * uy1); + float mdx = mx - vx; + float mdy = my - vy; + float miterDist = MathF.Sqrt((mdx * mdx) + (mdy * mdy)); + float limit = halfWidth * miterLimit; + if (miterDist <= limit) + { + this.EmitOutlineEdge(ofx, ofy, mx, my, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(mx, my, otx, oty, bandTopFixed, bandBottomFixed); + miterHandled = true; + } + else if (lineJoin == LineJoin.Miter) + { + // Clipped miter: blend between bevel and full miter at the limit distance. + float bdx = ((oax + obx) * 0.5f) - vx; + float bdy = ((oay + oby) * 0.5f) - vy; + float bdist = MathF.Sqrt((bdx * bdx) + (bdy * bdy)); + float blend = Math.Clamp((limit - bdist) / (miterDist - bdist), 0f, 1f); + float cx1 = ofx + ((mx - ofx) * blend); + float cy1 = ofy + ((my - ofy) * blend); + float cx2 = otx + ((mx - otx) * blend); + float cy2 = oty + ((my - oty) * blend); + this.EmitOutlineEdge(ofx, ofy, cx1, cy1, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(cx1, cy1, cx2, cy2, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(cx2, cy2, otx, oty, bandTopFixed, bandBottomFixed); + miterHandled = true; + } + } + } + + if (!miterHandled) + { + if (lineJoin is LineJoin.Round or LineJoin.MiterRound) + { + float sa = MathF.Atan2(ofy - vy, ofx - vx); + float ea = MathF.Atan2(oty - vy, otx - vx); + float sweep = ea - sa; + if (sweep > MathF.PI) + { + sweep -= MathF.PI * 2f; + } + + if (sweep < -MathF.PI) + { + sweep += MathF.PI * 2f; + } + + int steps = Math.Max(4, (int)MathF.Ceiling(MathF.Abs(sweep) * halfWidth * 0.5f)); + float da = sweep / steps; + float pax = ofx; + float pay = ofy; + for (int s = 1; s <= steps; s++) + { + float cax, cay; + if (s == steps) + { + cax = otx; + cay = oty; + } + else + { + float a = sa + (da * s); + cax = vx + (MathF.Cos(a) * halfWidth); + cay = vy + (MathF.Sin(a) * halfWidth); + } + + this.EmitOutlineEdge(pax, pay, cax, cay, bandTopFixed, bandBottomFixed); + pax = cax; + pay = cay; + } + } + else + { + // Bevel. + this.EmitOutlineEdge(ofx, ofy, otx, oty, bandTopFixed, bandBottomFixed); + } + } + } + + /// + /// Expands a cap descriptor into cap geometry edges (butt, square, or round). + /// Ported from the GPU StrokeExpandComputeShader cap logic. + /// + private void ExpandCapEdge( + in StrokeEdgeData edge, + float halfWidth, + LineCap lineCap, + bool isStart, + int bandTopFixed, + int bandBottomFixed) + { + float cx = edge.X0; + float cy = edge.Y0; + float ax = edge.X1; + float ay = edge.Y1; + float dx, dy; + if (isStart) + { + dx = ax - cx; + dy = ay - cy; + } + else + { + dx = cx - ax; + dy = cy - ay; + } + + float len = MathF.Sqrt((dx * dx) + (dy * dy)); + if (len < 1e-6f) + { + return; + } + + float dirX = dx / len; + float dirY = dy / len; + float nx = -dirY * halfWidth; + float ny = dirX * halfWidth; + float lx = cx + nx; + float ly = cy + ny; + float rx = cx - nx; + float ry = cy - ny; + + if (lineCap == LineCap.Butt) + { + if (isStart) + { + this.EmitOutlineEdge(rx, ry, lx, ly, bandTopFixed, bandBottomFixed); + } + else + { + this.EmitOutlineEdge(lx, ly, rx, ry, bandTopFixed, bandBottomFixed); + } + } + else if (lineCap == LineCap.Square) + { + float ox, oy; + if (isStart) + { + ox = -dirX * halfWidth; + oy = -dirY * halfWidth; + } + else + { + ox = dirX * halfWidth; + oy = dirY * halfWidth; + } + + float lxe = lx + ox; + float lye = ly + oy; + float rxe = rx + ox; + float rye = ry + oy; + + if (isStart) + { + this.EmitOutlineEdge(rx, ry, rxe, rye, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(rxe, rye, lxe, lye, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(lxe, lye, lx, ly, bandTopFixed, bandBottomFixed); + } + else + { + this.EmitOutlineEdge(lx, ly, lxe, lye, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(lxe, lye, rxe, rye, bandTopFixed, bandBottomFixed); + this.EmitOutlineEdge(rxe, rye, rx, ry, bandTopFixed, bandBottomFixed); + } + } + else + { + // Round cap. + float sa, sx, sy, ex, ey; + if (isStart) + { + sa = MathF.Atan2(ry - cy, rx - cx); + sx = rx; + sy = ry; + ex = lx; + ey = ly; + } + else + { + sa = MathF.Atan2(ly - cy, lx - cx); + sx = lx; + sy = ly; + ex = rx; + ey = ry; + } + + float sweep = MathF.Atan2(ey - cy, ex - cx) - sa; + if (sweep > 0f) + { + sweep -= MathF.PI * 2f; + } + + int steps = Math.Max(4, (int)MathF.Ceiling(MathF.Abs(sweep) * halfWidth * 0.5f)); + float da = sweep / steps; + float pax = sx; + float pay = sy; + for (int s = 1; s <= steps; s++) + { + float cax, cay; + if (s == steps) + { + cax = ex; + cay = ey; + } + else + { + float a = sa + (da * s); + cax = cx + (MathF.Cos(a) * halfWidth); + cay = cy + (MathF.Sin(a) * halfWidth); + } + + this.EmitOutlineEdge(pax, pay, cax, cay, bandTopFixed, bandBottomFixed); + pax = cax; + pay = cay; + } + } + } + /// /// Converts accumulated cover/area tables into non-zero coverage span callbacks. /// @@ -2075,6 +3125,47 @@ public EdgeData(int x0, int y0, int x1, int y1) } } + /// + /// Stroke centerline edge descriptor used for per-band parallel stroke expansion. + /// + /// + /// + /// Each descriptor represents one centerline edge with associated join/cap metadata. + /// During rasterization, each descriptor is expanded into outline polygon edges that + /// are rasterized directly via . + /// + /// + /// The layout mirrors the GPU StrokeExpandComputeShader edge format: + /// + /// Side edge (flags=0): (X0,Y0)→(X1,Y1) is the centerline segment. + /// Join edge (): (X0,Y0) is the vertex, (X1,Y1) is the previous endpoint, (AdjX,AdjY) is the next endpoint. + /// Cap edge (/): (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + /// + /// All coordinates are in scanner-local float space (relative to interest top-left with sampling offset). + /// + internal readonly struct StrokeEdgeData + { + public readonly float X0; + public readonly float Y0; + public readonly float X1; + public readonly float Y1; + public readonly float AdjX; + public readonly float AdjY; + public readonly StrokeEdgeFlags Flags; + + public StrokeEdgeData(float x0, float y0, float x1, float y1, StrokeEdgeFlags flags, float adjX = 0, float adjY = 0) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.Flags = flags; + this.AdjX = adjX; + this.AdjY = adjY; + } + } + /// /// Reusable per-worker scratch buffers used by tiled and sequential band rasterization. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs b/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs new file mode 100644 index 000000000..27156fde8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Bit flags identifying the type of a stroke edge descriptor. +/// Values match the WGSL shader constants in StrokeExpandComputeShader. +/// +[Flags] +public enum StrokeEdgeFlags +{ + /// + /// Side edge: (X0,Y0)→(X1,Y1) is a centerline segment. + /// + None = 0, + + /// + /// Join at a contour vertex. + /// (X0,Y0) is the vertex, (X1,Y1) is the previous endpoint, + /// (AdjX,AdjY) is the next endpoint. + /// + Join = 32, + + /// + /// Start cap on an open contour. + /// (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + CapStart = 64, + + /// + /// End cap on an open contour. + /// (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + CapEnd = 128, +} From 5ac7af409e412826ad7297b3349fc08dc1a11261 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 15:07:57 +1000 Subject: [PATCH 109/136] Update docs --- .../WEBGPU_BACKEND_PROCESS.md | 38 ++++++++++-- .../Processing/Backends/PolygonScanning.MD | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index a56f8d057..823bc49c5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -67,6 +67,30 @@ DrawingCanvasBatcher.Flush() -> on any GPU failure: scene-scoped fallback (DefaultDrawingBackend) ``` +## Stroke Processing + +For stroke definitions (`CompositionCoverageDefinition.IsStroke`), the backend +performs stroke expansion on the GPU using `StrokeExpandComputeShader`: + +1. **Dash splitting** (CPU): If the definition has a dash pattern, `DashPathSplitter.SplitDashes()` + (shared with `DefaultDrawingBackend` in the core project) segments the centerline into + open dash sub-paths before edge building. + +2. **Centerline edge building** (CPU): `path.Flatten()` produces contour vertices. + Centerline edges are built as `GpuEdge` structs with `StrokeEdgeFlags` indicating + the edge type (`None` for side edges, `Join`, `CapStart`, `CapEnd`). Join edges + carry adjacent vertex coordinates in `AdjX`/`AdjY`. Centerline edges are band-sorted + with Y expansion of `halfWidth * max(miterLimit, 1)`. + +3. **GPU stroke expansion**: One `StrokeExpandCommand` per band dispatches the compute + shader. Each thread expands one centerline edge into outline edges written to + per-band output slots via atomic counters. Output buffer size is computed by + `ComputeOutlineEdgesPerCenterline()` which accounts for join/cap type and arc + step count for round joins/caps. + +4. **Rasterization**: The generated outline edges are band-sorted and rasterized + by the composite shader's fill path (same fixed-point scanline rasterizer). + ## GPU Buffer Layout ### Edge Buffer (`coverage-aggregated-edges`) @@ -77,10 +101,8 @@ Each edge is a 32-byte `GpuEdge` struct (sequential layout): |---|---|---| | X0, Y0 | i32 | Start point in 24.8 fixed-point | | X1, Y1 | i32 | End point in 24.8 fixed-point | -| MinRow | i32 | First pixel row touched (clamped to interest) | -| MaxRow | i32 | Last pixel row touched (clamped to interest) | -| CsrBandOffset | u32 | Start index into CSR offsets for this definition | -| DefinitionEdgeStart | u32 | Edge index offset for this definition in merged buffer | +| Flags | StrokeEdgeFlags | Stroke edge type (None/Join/CapStart/CapEnd) | +| AdjX, AdjY | i32 | Auxiliary coords (join adjacent vertex) | ### CSR Buffers @@ -149,4 +171,10 @@ Coverage rasterization and compositing are fused into a single compute dispatch. Edge preparation (path flattening, fixed-point conversion, CSR construction) runs on the CPU. The `path.Flatten()` cost is shared with the CPU rasterizer pipeline. CSR construction is three passes over the edge set: count, prefix sum, scatter. -For the benchmark workload (7200x4800 US states GeoJSON polygon, 2px stroke, ~262K edges), NativeSurface performance is at parity with the CPU rasterizer (~28ms). +Both the CPU and GPU backends use per-band parallel stroke expansion — the CPU +via `DefaultRasterizer.RasterizeStrokeRows` and the GPU via +`StrokeExpandComputeShader`. Both share the same `StrokeEdgeFlags` enum and +`DashPathSplitter` (in the core project). The CPU backend fuses stroke expansion +directly into the rasterizer's band loop, while the GPU backend uses a separate +compute dispatch that writes outline edges into pre-allocated per-band output +slots sized by `ComputeOutlineEdgesPerCenterline()`. diff --git a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD index c97babb31..8d35bc744 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD @@ -17,6 +17,8 @@ https://github.com/aurimasg/blaze (MIT-Licensed) ## High-Level Pipeline +### Fill Path (`RasterizeRows`) + ``` IPath | @@ -43,6 +45,65 @@ Choose execution mode: +--> Invoke rasterizer callback per dirty row ``` +### Stroke Path (`RasterizeStrokeRows`) + +Stroke rasterization fuses stroke expansion with coverage rasterization so that +each parallel band only expands the centerline edges that overlap it. This avoids +the cost of a serial full-path `GenerateOutline()` call and eliminates the +intermediate `IPath` allocation for the expanded outline. + +For dashed strokes, `DashPathSplitter` splits the centerline into dash segments +on the CPU before passing the result through the same per-band stroke expansion +pipeline. + +``` +IPath (centerline) + | + +--> [if dashed] DashPathSplitter.SplitDashes(path, strokeWidth, pattern) + | + v +path.Flatten() -> List (preserving open/closed state) + | + v +BuildStrokeEdgeTable(contours) -> StrokeEdgeData[] + | For each contour: + | - Closed: N side edges + N join descriptors = 2N descriptors + | - Open: (N-1) side edges + (N-2) joins + 2 caps = 2N-1 descriptors + | Each descriptor carries StrokeEdgeFlags (None/Join/CapStart/CapEnd) + | + v +TryBuildBandSortedStrokeEdges(edges, expansion) + | Band-sort with Y expansion = halfWidth * max(miterLimit, 1) + | to ensure join/cap geometry reaches all overlapping bands + | + v +Choose execution mode: + | + +--> Parallel row-tiles + | | + | +--> Per tile: ExpandStrokeEdges -> EmitOutlineEdge -> RasterizeLine + | +--> EmitCoverageRows -> ordered emit via output buffer + | + +--> Sequential band loop + | + +--> Per band: ExpandStrokeEdges -> EmitOutlineEdge -> RasterizeLine + +--> EmitCoverageRows -> direct callback +``` + +#### Stroke Edge Expansion (`ExpandStrokeEdges`) + +Each `StrokeEdgeData` descriptor is expanded into outline edges based on its +`StrokeEdgeFlags`, mirroring the GPU `StrokeExpandComputeShader`: + +| Flag | Expansion | Outline edges | +|------|-----------|---------------| +| `None` (side) | Two edges offset by stroke normal | 2 | +| `Join` | Inner bevel + outer join (miter/round/bevel) | 2-N (round scales with width) | +| `CapStart`/`CapEnd` | Cap geometry (butt/square/round) | 1-N (round scales with width) | + +Outline edges are converted from float to 24.8 fixed-point, clipped to band +bounds, and fed directly to `RasterizeLine` — no intermediate edge buffer. + ## Coordinate System and Precision - Geometry is transformed to scanner-local coordinates: From fc1c65c75dd39e2ab1e454a355cb48a064df4558 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 15:17:27 +1000 Subject: [PATCH 110/136] Update reference images --- .../Drawing/DrawPolygon.cs | 19 ++++-------------- ..._RegionAndPath_MatchesReference_Rgba32.png | 4 ++-- ...r_WithClipPath_MatchesReference_Rgba32.png | 4 ++-- ...calCoordinates_MatchesReference_Rgba32.png | 4 ++-- ...StateIsolation_MatchesReference_Rgba32.png | 4 ++-- ...hesReference_Rgba32_ColrV1-draw-glyphs.png | 4 ++-- ...atchesReference_Rgba32_Svg-draw-glyphs.png | 4 ++-- ...thAndTransform_MatchesReference_Rgba32.png | 4 ++-- ...imitiveHelpers_MatchesReference_Rgba32.png | 4 ++-- ...PathWithOrigin_MatchesReference_Rgba32.png | 4 ++-- ..._FillAndStroke_MatchesReference_Rgba32.png | 4 ++-- ...eMetricsGuides_MatchesReference_Rgba32.png | 4 ++-- ...awText_PenOnly_MatchesReference_Rgba32.png | 4 ++-- ...AndLineSpacing_MatchesReference_Rgba32.png | 4 ++-- ...izeOutputFalse_MatchesReference_Rgba32.png | 4 ++-- ...aw_PathBuilder_MatchesReference_Rgba32.png | 4 ++-- ...ndGradientPens_MatchesReference_Rgba32.png | 4 ++-- ...enOddVsNonZero_MatchesReference_Rgba32.png | 4 ++-- ...backCapability_MatchesReference_Rgba32.png | 4 ++-- ...backCapability_MatchesReference_Rgba32.png | 3 --- .../Process_Path_MatchesReference_Rgba32.png | 4 ++-- ...MultipleStates_MatchesReference_Rgba32.png | 4 ++-- ...store_ClipPath_MatchesReference_Rgba32.png | 4 ++-- ...zontal_Rgba32_Blank100x100_type-spiral.png | 4 ++-- ...ntal_Rgba32_Blank120x120_type-triangle.png | 4 ++-- ...zontal_Rgba32_Blank350x350_type-circle.png | 4 ++-- ...ical_Rgba32_Blank250x250_type-triangle.png | 4 ++-- ...rtical_Rgba32_Blank350x350_type-circle.png | 4 ++-- ...-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 4 ++-- ...-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 4 ++-- .../DrawBeziers_HotPink_A150_T5.png | 4 ++-- .../DrawBeziers_HotPink_A255_T5.png | 4 ++-- .../DrawBeziers_Red_A255_T3.png | 4 ++-- .../DrawBeziers_White_A255_T1.5.png | 4 ++-- .../DrawBeziers_White_A255_T15.png | 4 ++-- .../DrawComplexPolygon.png | 4 ++-- .../DrawComplexPolygon__Dashed.png | 4 ++-- .../DrawComplexPolygon__Overlap.png | 4 ++-- .../DrawComplexPolygon__Transparent.png | 4 ++-- .../DrawLinesInvalidPoints_Rgba32_T(1).png | 4 ++-- ...sInvalidPoints_Rgba32_T(1)_NoAntialias.png | 4 ++-- .../DrawLinesInvalidPoints_Rgba32_T(5).png | 4 ++-- ...sInvalidPoints_Rgba32_T(5)_NoAntialias.png | 4 ++-- ...Dot_Rgba32_Black_A(1)_T(5)_NoAntialias.png | 4 ++-- ...ot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png | 2 +- ...ash_Rgba32_White_A(1)_T(5)_NoAntialias.png | 2 +- ...gba32_LightGreen_A(1)_T(5)_NoAntialias.png | 4 ++-- ...nes_EndCapButt_Rgba32_Yellow_A(1)_T(5).png | 4 ++-- ...es_EndCapRound_Rgba32_Yellow_A(1)_T(5).png | 4 ++-- ...s_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png | 4 ++-- ...intStyleMiter_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- ...intStyleRound_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- ...ntStyleSquare_Rgba32_Yellow_A(1)_T(10).png | 4 ++-- ...awLines_Simple_Bgr24_Yellow_A(1)_T(10).png | 4 ++-- ...Lines_Simple_Rgba32_White_A(0.6)_T(10).png | 4 ++-- ...wLines_Simple_Rgba32_White_A(1)_T(2.5).png | 4 ++-- .../DrawPathCircleUsingAddArc_359.png | 4 ++-- .../DrawPathCircleUsingAddArc_360.png | 4 ++-- .../DrawPathCircleUsingArcTo_False.png | 4 ++-- .../DrawPathCircleUsingArcTo_True.png | 4 ++-- .../DrawPathClippedOnTop.png | 4 ++-- ...endingOffEdgeOfImageShouldNotBeCropped.png | 4 ++-- .../DrawPath_HotPink_A150_T5.png | 4 ++-- .../DrawPath_HotPink_A255_T5.png | 4 ++-- .../DrawPath_Red_A255_T3.png | 4 ++-- .../DrawPath_White_A255_T1.5.png | 4 ++-- .../DrawPath_White_A255_T15.png | 4 ++-- ...sformed_Rgba32_BasicTestPattern100x100.png | 4 ++-- .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | 4 ++-- .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | 4 ++-- ...sformed_Rgba32_BasicTestPattern250x350.png | 4 ++-- .../FillPathCanvasArcs.png | 4 ++-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 ++-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 ++-- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 ++-- ...oJson_Mississippi_Lines_PixelOffset(0).png | 4 ++-- ...on_Mississippi_Lines_PixelOffset(5500).png | 4 ++-- ...vgPath_Rgba32_Blank100x100_type-arrows.png | 4 ++-- ...erSvgPath_Rgba32_Blank110x50_type-wave.png | 4 ++-- ...derSvgPath_Rgba32_Blank110x70_type-zag.png | 4 ++-- ...SvgPath_Rgba32_Blank500x400_type-bumpy.png | 4 ++-- ..._Rgba32_Blank500x400_type-chopped_oval.png | 4 ++-- ...gPath_Rgba32_Blank500x400_type-pie_big.png | 4 ++-- ...ath_Rgba32_Blank500x400_type-pie_small.png | 4 ++-- ...verageCache_SkiaBackend_RepeatedGlyphs.png | 3 --- ...eparedCoverage_DefaultBackend_DrawText.png | 3 --- ...sPreparedCoverage_SkiaBackend_DrawText.png | 3 --- ...t_DrawPath_Stroke_LineCap_Butt_Default.png | 4 ++-- ..._DrawPath_Stroke_LineCap_Round_Default.png | 4 ++-- ...DrawPath_Stroke_LineCap_Square_Default.png | 4 ++-- ...Stroke_LineCap_Square_WebGPU_CPURegion.png | 4 ++-- ...ke_LineCap_Square_WebGPU_NativeSurface.png | 4 ++-- ...DrawPath_Stroke_LineJoin_Bevel_Default.png | 4 ++-- ...Stroke_LineJoin_Bevel_WebGPU_CPURegion.png | 4 ++-- ...ke_LineJoin_Bevel_WebGPU_NativeSurface.png | 4 ++-- ...th_Stroke_LineJoin_MiterRevert_Default.png | 4 ++-- ..._LineJoin_MiterRevert_WebGPU_CPURegion.png | 4 ++-- ...eJoin_MiterRevert_WebGPU_NativeSurface.png | 4 ++-- ...ath_Stroke_LineJoin_MiterRound_Default.png | 4 ++-- ...e_LineJoin_MiterRound_WebGPU_CPURegion.png | 4 ++-- ...neJoin_MiterRound_WebGPU_NativeSurface.png | 4 ++-- ...DrawPath_Stroke_LineJoin_Miter_Default.png | 4 ++-- ...Stroke_LineJoin_Miter_WebGPU_CPURegion.png | 4 ++-- ...ke_LineJoin_Miter_WebGPU_NativeSurface.png | 4 ++-- ...DrawPath_Stroke_LineJoin_Round_Default.png | 4 ++-- ...sDefaultOutput_DrawPath_Stroke_Default.png | 4 ++-- ...easesPreparedCoverage_DrawText_Default.png | 4 ++-- ...d_MatchesDefaultOutput_Process_Default.png | 4 ++-- .../ReferenceOutput/Drawing/optimize-all.cmd | 1 - .../ReferenceOutput/Drawing/optipng.exe | Bin 103424 -> 0 bytes ...d300x300_(255,255,255,255)_scale-0.003.png | 4 ++-- ...lid300x300_(255,255,255,255)_scale-0.3.png | 3 --- ...lid300x300_(255,255,255,255)_scale-0.7.png | 3 --- ...Solid300x300_(255,255,255,255)_scale-1.png | 3 --- ...Solid300x300_(255,255,255,255)_scale-3.png | 3 --- ...d300x300_(255,255,255,255)_scale-0.003.png | 4 ++-- ...lid300x300_(255,255,255,255)_scale-0.3.png | 4 ++-- ...lid300x300_(255,255,255,255)_scale-0.7.png | 4 ++-- ...Solid300x300_(255,255,255,255)_scale-1.png | 4 ++-- ...Solid300x300_(255,255,255,255)_scale-3.png | 4 ++-- ...Rgba32_Solid2084x2084_(138,43,226,255).png | 4 ++-- .../Issue_367/BrushAndTextAlign_Rgba32.png | 4 ++-- ...d492x360_(255,255,255,255)_ColrV1-fill.png | 4 ++-- ...olid492x360_(255,255,255,255)_Svg-fill.png | 4 ++-- 128 files changed, 236 insertions(+), 272 deletions(-) delete mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/optimize-all.cmd delete mode 100644 tests/Images/ReferenceOutput/Drawing/optipng.exe delete mode 100644 tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png delete mode 100644 tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png delete mode 100644 tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png delete mode 100644 tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index e01f01d8b..b7f3448b0 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -195,28 +195,17 @@ public void SystemDrawing() public void ImageSharp() => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); - [Benchmark(Description = "ImageSharp Combined Paths WebGPU Backend")] - public void ImageSharpCombinedPathsWebGPUBackend() + [Benchmark] + public void ImageSharpCWebGPUMemoryBuffer() => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); - [Benchmark(Description = "ImageSharp Combined Paths WebGPU NativeSurface")] - public void ImageSharpCombinedPathsWebGPUNativeSurface() + [Benchmark] + public void ImageSharpWebGPUNativeSurface() { using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, new DrawingOptions()); canvas.Draw(this.isPen, this.imageSharpPath); canvas.Flush(); } - - [Benchmark] - public IPath ImageSharpStroke() => this.isPen.GeneratePath(this.imageSharpPath); - - [Benchmark] - public void FillPolygon() - => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(this.strokedImageSharpPath, Processing.Brushes.Solid(Color.White)))); - - [Benchmark] - public void FillPolygonWebGPUBackend() - => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(this.strokedImageSharpPath, Processing.Brushes.Solid(Color.White)))); } public class DrawPolygonAll : DrawPolygon diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png index 7324dfbe0..a7d443758 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63523bfb277d9f0db0c5a58fdd8d8a0a14d26537abece16322325bd9faac1e9a -size 3910 +oid sha256:39408da1bb6e4003ff0c2977b467fe169a07ff8797b5f99ec5d959cec298eff9 +size 3904 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png index 9bc8bbcc6..947c995b2 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4bca6efabaacd96d852a06a62a3e966c589463cd4e977923e39101461e4f825 -size 10930 +oid sha256:95492c39205f3e593703e7e70e80495c319e196e31c3d1749df5e6c644375a10 +size 11117 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png index 8747b0439..763a40e15 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:764948d06820b75dfcbf9341fb5b30c3b5a043022dce2932214d3b83b42d2718 -size 2070 +oid sha256:33d2c4be22e1e7968f7c555dd3d39f927dbed3b3e960ca55fd197d5830720eb2 +size 2023 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png index 7073fa8d2..c8eab3396 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90edf38f6d93d8c9142172a33b1e94948530984f250677f201e4af382ff091f1 -size 13865 +oid sha256:3de381e0e961b43c1d9275adc78bd7c3fbe8f445d6d256c65a1945407b40c606 +size 12394 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png index f174b8178..08cc87986 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0981d21ed8f75ee1fffefdae75505365bea3715bfbf8bda56d278792766f9b09 -size 10939 +oid sha256:cc680ab6ee1d5d28f6b35e959402d0ee28f314a9cfc5df39cb04f234e9b51b8f +size 11082 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png index 681c0543a..829180a52 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53206cc3329cb5a49afd48cb17a98a4ced8b38bc6f0b90b5f02e647b0f23e8ee -size 10940 +oid sha256:5ab6a73fbda08c85580c7f326fb53ac3e51aa16ff6c16d95813e4feee29b5fb0 +size 11082 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png index 1ea69c2f0..ff66131a7 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:489da8aa1349a55de5086019a88357114a6ff1576038cdf975894d6648f4b7fa -size 11081 +oid sha256:266c91d9bdbb7714fbe84f372e1998220a1794f7b473cac4ce51e57b114a9052 +size 11333 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png index ddd97673e..5d9ac768f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1b8f27b7844739e6fc64e0e63e5dec5e93f72bec2e9c5f11d7f9ccda0788488 -size 9134 +oid sha256:ce7f2b5f7442f07d112cf5fd623a257b054953e5663d29263eb33e40bcb25ddf +size 9196 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png index d636d4bf2..5915a5a10 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a95282d25ea59eac6d996f574b7f1167c50f2dffeef719594f2cf84bbef17652 -size 11049 +oid sha256:4907effeaccf759f2bdc47e589427a0b49a7264959934978b304058aaf79ed1c +size 11078 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png index 69d2f6f0d..fc09d3204 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab9520ac162d3a40a8b96441c4fe5e83a3b8142d89f17ec41161a37afa30aad7 -size 21366 +oid sha256:9f4dfe9dcd6cd545d9f7fe407f37c8b473667021dec638447bc378d2ce2a7f1a +size 21028 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png index 471d4af04..f1176f361 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e52723224dda43093a5e1f1330abe9fed214c5308772278b4c3791d7bd0f2a0 -size 26116 +oid sha256:b77316ee5bc96af4106953c71180a1e16d8740e1a9d64011513e48d06b200af6 +size 26419 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png index 71c0cc3f2..830aad8ab 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1d471409c0c0eae66ea5f92bfef64da1de8808f132b4746282874d9b2740c7c -size 3203 +oid sha256:de3bd81004a0454d382aaaf21b74adee9114a3427c8db693f952e266da1511b6 +size 3211 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png index baafc25f3..c03a6cf88 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ace94fdd7f0a55cb5427f879c1ce8fdd7693a594e50c6f720df7611dfb28f78 -size 45434 +oid sha256:616d67dcd42fa592004096d7f76fd2248875018c1d21c2502e56f18d32b33e52 +size 45728 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png index ac71bf251..c17c63c2c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b9061fe07c30c4d8ad28dde127ef4153d0a522d3c3040184baafa7b7c8782c7 -size 3556 +oid sha256:06c8db662ea35a092d6dc2e8026a3ee6a0ae7d965406a64b3ae9dd3054a81031 +size 3358 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png index 75ee17c04..f318c18d7 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb8583ce253d500309296ac2bbee670a46aaa56162f5103305d22f02d1e90a1 -size 3449 +oid sha256:0ab4c143dc8dc98359abce0e1a89cbd77a6224fe4dab44a35a4164222db365d4 +size 3439 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png index 05629b290..c5fbb315f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4d703eefd1e0c88bc6bd952382260179f513d2f9c65c9b0ef25943ae8d1e6b2 -size 11158 +oid sha256:bfca64f26265f02d1912c943b09c47a956d8f7cf1b57b64dbdcc0175d25610c7 +size 11611 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png index 9b70ed9c3..7c1ab4ae4 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3940441662db9d40c73733bb7c3f54be823dd4a12a1458bc24ac4b0fc05d01ed -size 8271 +oid sha256:3c63a3f5fc2abf1879f891590f5fd2d85be120f3109d1cf46a264b64e6ac8e7f +size 8188 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png index 36b58fc84..11493e17c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 -size 12961 +oid sha256:3c2b561014aa035424a8745a1c20de3ce33efb5d11137b065a9ec43eed279dd8 +size 12884 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png deleted file mode 100644 index 096f34c82..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithoutReadbackCapability_MatchesReference_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:233b4d389e5b1a1c9cca4ba99769a7d49b74d3d3c1a14d5e004d11fd8052d49e -size 12939 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png index 36b58fc84..11493e17c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 -size 12961 +oid sha256:3c2b561014aa035424a8745a1c20de3ce33efb5d11137b065a9ec43eed279dd8 +size 12884 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png index 89eb299d0..d2c44a3c0 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:159624425a1310a73dcef13263440546f2778c0bdc1871d8f36ae8bda7325907 -size 4889 +oid sha256:f08dfc8106c343c0e2af1e76caccf0fbb1dd01f9940b7f8fcc67679ae47161c5 +size 4881 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png index 812ec0e58..51474a423 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:152c8529b7e299e6024c33a769a0171846d8e09ce8756be2fe509d17e26f87d2 -size 1342 +oid sha256:568e53bd59a24331d0aa6c07772819e1c3482f8826d7e373650dab548d8abb55 +size 1332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png index 5b5dc4529..a7585cd7a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c50feec7f3eb4a9dde88462398c46af6841aa4f27bff943858be8219d03d31f -size 5299 +oid sha256:2195ff2337653059cd3ff9d13ffc7301e23f7e5adc7a618a2404b0a1fb247ffb +size 5338 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index a187c316a..3c5316a46 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9ef4be7abbe91ecbdbdc83345fc2622c904291f4df3c260cf4d4ddc78cf48fe -size 4379 +oid sha256:e0c3b48846b5b64a99de9cf1c76bfaa6bfa9f6644803e0139543c2d4e82c58fa +size 4377 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png index d2d64f8b5..8134fe060 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abb325c92147f9810d04059a1ea24e6be9e7dd0471613a16df266371e25f6f10 -size 9390 +oid sha256:6395ec412e0b912d693d84458785b7749baf7e2a35dde1b9263af69dd602276c +size 9528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png index 2cfa75908..d9a22ed35 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61c21eaea8f0bb4676954df6576aa7cf70a398297b92ac9e247883347d08263a -size 5181 +oid sha256:c861fe15240f0d19e614cfae29abe239a9b51b14c0789b681a01298bba43f4b6 +size 5180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png index dc6115863..e14fabeaf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffe197264326acae59f95a1021f645c423b755f2e9feccc7d284a90c2e0a275f -size 7395 +oid sha256:eaa00cbf0f654adaae06c81fe72ac18bfa0dd165dec92e1e7435ac7d04aff692 +size 7443 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png index 27d3ab6d4..4f4e3224b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bab6a51f70ce450693978a78a94488978f13c0f4e36ef5bda10c21fd59c4d108 -size 2589 +oid sha256:df40aa73e47380e3c4e645f17b674c25432c5c0657f29735a8401e15e3411a40 +size 2604 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png index 6bdb2b7a6..86a02b332 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:127377f85a93c6b8d4f3836248e89eefad399d39abe328d85693b4e0997eb8ff -size 2500 +oid sha256:519661ae4e1e4077e3515de5fdb5ae605369dafe780c8cfb2acfe2f1ebff77d7 +size 2482 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png index 13f33766a..1850fa1be 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:174c98c137feb54c05fa59823af2a09fdade5d2ceb59e70e37c507dafcf6118f -size 4334 +oid sha256:5e30c532eae115e1b919b53087695214e42b45bb35179af6e42a3132af05dff4 +size 4325 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png index 5d699c129..8a7c46329 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78b010bd2d394114df808155d622ae18e786de07af063942d97d4ae849b3ad83 -size 4627 +oid sha256:6c17958516f87e456dd79df1f546171ba6f35b32d6b99e5fcaee1f3937037886 +size 4585 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png index 58142a3f5..2a59cc9ce 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:340eec740605d3f94801c1db0d8de14043fbcd3b2752a9edee9e60c4205c4ba5 -size 4627 +oid sha256:65b8cd9c25a68e6ab6fc836cc1e4a6e3ffed3e266bad5f7de0685ce163930933 +size 4585 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png index cac478b4e..e0d763c56 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d83743330f8b3d1a36929118c939af29f58c9ffa9a133b49f4e8a42ce657712a -size 4624 +oid sha256:9f550fc772fbd1a4295a51fdbabb26c1148c5d28fb4eb863a321b35aa6a75ccc +size 4581 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png index cac478b4e..e0d763c56 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d83743330f8b3d1a36929118c939af29f58c9ffa9a133b49f4e8a42ce657712a -size 4624 +oid sha256:9f550fc772fbd1a4295a51fdbabb26c1148c5d28fb4eb863a321b35aa6a75ccc +size 4581 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png index 2f18d1dcf..9fba12d05 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd9d8ca37fda3431ade9c1b5bdf2c2cb77baccd33f082a3191c57685b175d568 -size 3845 +oid sha256:f431c391d1283c0ade7cd188d99852d30d90accf23f33febb28f1058318d2b3c +size 3657 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png index 60af1f394..6b8971645 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:213c26fc13a8f5faffdea4da6892c88c47d63b693a3ccd4187863b83869dbea8 -size 8195 +oid sha256:4a6833ea382684b0cc99c56119cfb4ebf450fd952551d33fcd62c134072ab92f +size 7809 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png index e660027ba..ab6f9a348 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ca0865255e4b5091ede748dfd964900745cf2ac7a83f64170119f44a4b9dd2f -size 5748 +oid sha256:81c7550b1737ecf92a84363ff4f830562be73e8ab43bd6b3656160793f1448d8 +size 5799 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png index 1c10a0e69..bcc40e163 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fcdb023797fee203cafe3e184a2492e915f5d141f9149d5037b72072628071f -size 3712 +oid sha256:a4e81d12ddbab8962e1a5068da896cf4ca208336b4bfbf0bc468a94dbf0d41c4 +size 3624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png index 368f44ff6..6cd79f23b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 -size 90 +oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png index 368f44ff6..6cd79f23b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 -size 90 +oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png index b213ccca7..6cd79f23b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b -size 92 +oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png index b213ccca7..6cd79f23b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b -size 92 +oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png index 8c8f5d483..53f315caf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4571c05512d80efe9a04a9ef75ee14511f5cd0182ae1630347fd6d53cc4e30f5 -size 996 +oid sha256:0797ec93efa1dc5767ecd4531f63b79817f031b014ead37e26675594fee88923 +size 997 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png index cb1e62505..9131466bc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d6be109fd0778212b7c92ea4b83af2c9799d383e7cd674514251a7d1e20f45a +oid sha256:955772b67450ea0d9ef13f2b8754750939d3850f34d3b589ee653d2d5ca5af53 size 1011 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png index 15adb7c31..ade213629 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b54785fecd7ddc017876f0bdc0190e6a43987c2d69c5150a29b640172ae0d65 +oid sha256:c647187ebcf6ef794f463454c04855bb0e064a7ddb37dd62b0a1b0563305f394 size 1072 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png index 863cf7c7e..130bc7b49 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff0ce825507b2e95b49c8eebfde000b10c890e090cd7bb533397fbb62767178c -size 903 +oid sha256:d34de570982d91b9f1b6f836957e8bdd454c9867948084487bc260f54d50a0fc +size 905 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png index b3c12d506..a27612003 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb0493bc24df7d4dd74f02d9e7ad00074f5fd237ed05bf4a5bb36ab01588fa9 -size 2795 +oid sha256:a97f219e885cc7cc257cf2a06d1b75ba23b77c3cb286e2bdd2875ceee3f6c8af +size 2805 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png index 8ec380476..57c6b0fab 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3d0acf65b85c8e58096e281b309596d51785502e17d8620ca71ee36b5a46943 -size 4215 +oid sha256:456c72ce3d8da61161d7de3fb7d809e0c51a71c0e1b8a1ac68bffc25683eee4b +size 4200 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png index ac94cc581..7d2189407 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7eedeae9de91af7e3ceb21f4abde363c7becb175ac87d72506fefa7013cd708 -size 3162 +oid sha256:958f8999354475e6361b124575d8206d4bc17e3ae067bf4966bd7904219eeb7a +size 3037 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png index e5e3de9f4..835c242ce 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3415ade15c7cdce6b0f1ddbd8a944a8dde25d107306d66ae6b3afa96844a0dfd -size 2233 +oid sha256:cec5888ca8a55f6e5d561c27d7276fc463b68f6a7f727ddc15b98c7da515f831 +size 2192 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png index 72a2804a7..1c9a755bb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9850958e82712956717dfbaa54f30426628051b75f92461f8b8dabd5ae673177 -size 2256 +oid sha256:18380f5cf553eb6104a995336f8db55b1aaefb1fa7bbe7b971987f8d7c256e1d +size 2190 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png index 7df5c6d23..95e830cd4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f89b5cf4014bd7ad886e10bd92cde5c00ef00ab13c78062f7fe39318e8bbbc45 -size 2255 +oid sha256:8e176cf6bdb1df68a54e1633f2ac38f0034f0b76c4d8b3b2022be76fba6786ad +size 2187 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png index 5c4b1b9db..5c59b9b1f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ecccb5372b5d9f4e5198589eb053f869f498825c83e1cc9625c4857b3fa45c4 -size 2189 +oid sha256:ce1087527c29764c71a0afd1a7c99df50d48e977cda6c0739141978607d9d622 +size 2129 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png index df3d4e075..249ec8438 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a6ab83344a143b2160b1996307464a7c56f6b7ef3b1de8ecd1b588eca15713 -size 2172 +oid sha256:2b040917d9bc03bce714c5fbea276566e3c31ac76372f586be25c56f8de832ba +size 2175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png index d27625ceb..78d5f08bb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6316a1065534532b72d9975223b9714907690b39c20e09ed65d846c7dffc2417 -size 2183 +oid sha256:d42c76e47416d5fcf8669ffce6275a81139253ee0e7b577ce393cdb16a65152c +size 2012 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png index b0c830f95..b74588b7b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4fa5eec491aaf3ece0c1d6a842c667e172c1697eac725f4ef2672742b9b695a -size 3815 +oid sha256:470b04d20045a42386615f0e9ae91ddf55b6276e51458cc0360c180d89c790b2 +size 1787 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png index 96098fbf1..17ac889de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03eb9645a7fb021bd30723dc4a4a8b1bc2900f604fef165a6ac472bd0a25c537 -size 1713 +oid sha256:001414679819cfdf8b0f59a89646ed6bdb042e5008f899f1dad3819d213a19f9 +size 1718 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png index ceeae75f2..cc731a31a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8ba7c8929209a833524ff5cfb59ecded92d7a95b3022bbda80816aff313c31 -size 1559 +oid sha256:55dba01c08ac1096d3153dc4234932b02894442f1efe72a036b7ae56f53d21ad +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png index ceeae75f2..cc731a31a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8ba7c8929209a833524ff5cfb59ecded92d7a95b3022bbda80816aff313c31 -size 1559 +oid sha256:55dba01c08ac1096d3153dc4234932b02894442f1efe72a036b7ae56f53d21ad +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png index b63118945..47a0330ff 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c8a0a8dfad42d55f78144a6748634647a02c1076e5096c5ff1184c63ab8ea49 -size 206 +oid sha256:ecef2543ded88cec8ddbc9a34b1c01d45c315e6a21ec6501ce70797d97288c6d +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png index f9bb9b729..c47d700e5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aee03a50f20101e217cbcb7dcb227b94d52a8d09748a923e5f6f2e69c60f858f -size 5801 +oid sha256:5027c15dab6e0a3e2cb88f6dba73753b19fd2f74f6eff6aa530fdad7cb79ec77 +size 5787 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png index 6408b21ca..b9fbf99f6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf91c8aac027e1bbb9629eab3cd2f06db76e15281b6f7b93fbb6d2bfae78b151 -size 16016 +oid sha256:d84cf12e6831d73cb271ed5fbf2b22e52603ea6d95aaa1736fd86829dbcb7f94 +size 7739 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png index c1c9b090d..488e37a9b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cbd1507004337a94fd4121cd006c57d0ae52db55c34edf84d8caca94128a933 -size 16942 +oid sha256:e6225d9d921256a34c1d51b4a674953644380f7840d7ff5d514365e0e336d265 +size 15137 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png index e8f8954e4..6e6a1b77e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d9a8d0f6639a4f9cb1af2e3de0b9b7ba4829c5d2857682ce928ef81b669ea42 -size 14549 +oid sha256:b92fc8f096ef187d1e661c36dfefe1188d141f949f18948dd50c76fd8e273e4b +size 14367 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png index 5a228a7ce..9ea280b2a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c6da672f1b187d7296ad7443609582fc1eba9578d23d1005b71049e303a1452 -size 14856 +oid sha256:58bb5e0daa0f1dc3ae303189688d7b6044110916847d3eded5e31514d16fccf4 +size 7512 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png index 7f6128e54..fe3282db4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03f1eddf5f7f4b7244a1652211a8a271b8641aa25b5bb6306877194edd8e0f4d -size 7996 +oid sha256:c9ef81afbf079de5da44f0973a2a0515aa4c1899d7c2b892e91bb333a153a68b +size 7967 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png index 0632d49bb..74f687702 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74d9e27ef56c1783e335739185abc8163f7930f20d84605099045bd2ac1cbd0a -size 601 +oid sha256:03956bafa8a1d949c06df75c8b6f824f21b33335de7306c2ff06632fdb8b47b1 +size 588 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png index a857ca811..592cee017 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a894d928cefd7c78f8c3911d31cae00a9a97aef89be64568b0a16c68b1b8a3e -size 3558 +oid sha256:d883ca62ee26a0ffd7a68ed8aa171fe2a12b56337d8f7368f7c232b48531d8b1 +size 3114 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png index d2326aaa4..e8157776e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f435173db003d7ddb4da76fb94745b0ec38c519b4b46aa7e9808ba5129c4b8b -size 3405 +oid sha256:943e09dd6e4e80f8b76af7dc558e47e38c9abb21a40355d42f7fb82dc7744826 +size 3084 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png index f6b291b02..16d14ed74 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21daf8a4a2a072942ef46e4f7c7a572943c702eae801d45c5466f7d8a577692e -size 3537 +oid sha256:8c769b4e5e161d3a121f6ab01c4733452149568758b18fef2abe56cc351b1828 +size 3072 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png index 1995db5d3..e021f6f0e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c931830d335f15f3232b0f9ad51443155dc8e7bd0bf77538414165479719cad3 -size 8839 +oid sha256:a270570cfcc2c7a9da7d678de596c08cc3158d062f7a068debaf175c97f27153 +size 8834 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png index 2180c82a6..a5ee74244 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d096fcea8556aaf91eea17a31896527f10285c5d17e073dbe2715aadfa3bdcd5 -size 1500 +oid sha256:b93a5479c1efd00cf61b0e4a27e1e484cb98235033b365f3334321f82c53b668 +size 1459 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 3c560e659..5ef307a54 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb13ecdb980ccb0bc08df4e1628af9dd4a14368b17f717c1e571262836278b3a -size 17378 +oid sha256:f476d97fd14ff7ad932548229ef81ac0266180a312ece122fda9a534b8e72b8e +size 18165 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 32d0548e6..6464b040b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fbe28e31e46b156fe7017628b4cd530f8211bf16b4cb15c335e51e190ed7f77 -size 778 +oid sha256:ce7300e886038d4dae1e9b164ee455df2d51d3d367034de3050bfa0d7b0dc36a +size 776 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 8c9c2ecbf..68302ea94 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79319b83ce8c32bace057288383b441b8d2c9ae96c16e9ffc3d5e2a227d61b63 -size 16823 +oid sha256:2a6e8fd0ecc151ab8f6d3000904967f5d2cdcd011c5f7478b974ceeef6395769 +size 17554 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 673944323..e3435a9d1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ec38824fbaf150d9924f9d53b4fea2a5efef7cb782c81591079de4010a226fc -size 15109 +oid sha256:1d97aed86bc20ce47dd11ed7ca95c05c6950c59de375b25763c52f44834826eb +size 15485 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 4a3df2f4d..6366790f1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9637e8c6e2fb90bb4182c55daf13e604a03cf68ab36c79c2c97f016768df8df6 -size 726 +oid sha256:43cd0f06711e590857c4f11a35dccde09edcbb9187e7df2741d9560ae263ef85 +size 728 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index ac17b1fd5..e6a78aee9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71be9cac7c1015c4b9139433d7585b7073c6242bd7e1d7bb1c1194d6facdc8f8 -size 15495 +oid sha256:ba9405b6c8e73699185dd0ad75b560b4634faabf2c2cf5df2ce5f6834b984718 +size 15774 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index b58f0a478..7095cea88 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50bcdda1794425a3096665c4e8763bd89310e79bc331d8d28512d6d5a3f55e3a -size 4486 +oid sha256:512696c1aca0a9804434a7e46a6b5b3efab688ef7c9d22fa4657a7a8cb6ea90d +size 4588 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index 7d6f87f8f..112d017cd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a962b83d330b3f08ce403d9b8b110173367d2a075515280a356779cd96a3065 -size 41039 +oid sha256:fc4989cf7a88c1f07305649bf1809646ed116be2849fe0f0aad2e16a33f53f69 +size 41159 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png index 8ee9484f2..0e6e38803 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7b52e2255f8f1a7b72bb6a6937580d87cddd3b339a7894f5ec3f052b8485c64 -size 407 +oid sha256:52f5870479d19eedd37cc31a7a7b25fc3285f6466d524d46ecf85f85fa0f9e6d +size 393 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png index 9bdb776c9..db9f082e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:649424597ccdbdbfbdf0aeb85c83f232a5970a0a4322d7f1681df6b6ca45ec37 -size 681 +oid sha256:055c34ce5f5a1dc3c0ea4f806a3e4cf0ceae938a71ebd52564d6bb2fa7d4aeb5 +size 655 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png index 5e223bab7..175041966 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b9e8c04f5984bf697f0c45bd98c471e86d020bcd9068a0ba9b6a394dc62543 -size 451 +oid sha256:99d520aa8a1feb8abd21b63c8cb6f105383742830a3ebc3ca9c8b3deb64ba4ef +size 436 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png index ab555b0bc..c9f160d53 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efb39f74cc777f108ab9df20956796cf1568d06d60df8943769bf897993a06ca -size 4887 +oid sha256:3ea2dbafb56496657590e59a81f09446cb18b8573740e83ad80fb910b18e5017 +size 4847 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png index c19affb07..47e8c16ea 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a5e03190fa9497ccc55ad3923e7242428d78d533fa0f9dcf8965d06793d68a8 -size 2794 +oid sha256:b52f21326c9ebeb3429dc5f6a4e9a99bb33573da31be9582baa8ce1212c47387 +size 2769 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png index 562c76e6b..e1f9b87cb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb899460473490c06e3933b0a872ba6c05256e223896caadcdb462a93807d771 -size 2459 +oid sha256:09f2f38f2b595182dd24a0772c34c45567d8659a478132e97771cadea2c34b9c +size 2542 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png index 83661b2cc..3425de7aa 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2899498199c9b9c5de959be83a6d8091d8f75319efb8964b0610900e978c1144 -size 9603 +oid sha256:fa2f4adac7cd072ffb308ffa89392e3dfce3c41f67352a1701fff426a3d07c9e +size 4848 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png deleted file mode 100644 index 362818a52..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_SkiaBackend_RepeatedGlyphs.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5dfd12a315d54dad1931e83738fe84f9d681a90aeb6cd4df0886668ae78f686 -size 1752 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png deleted file mode 100644 index 6ffabd4f5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_DefaultBackend_DrawText.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:305b538606ca3005522be3b51e76128503427dd21a212ef59797e6d26553ec31 -size 36499 diff --git a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png b/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png deleted file mode 100644 index 8c5045037..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SkiaCoverageDrawingBackendTests/DrawText_WithSkiaCoverageBackend_RendersAndReleasesPreparedCoverage_SkiaBackend_DrawText.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e50f8be7a061a595eda81eda41dadc368f9fb1ed7044d03599147b612a7f302 -size 21154 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png index 98e4f91a6..b1654d308 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:408c8ac372fd34911814c3f4500542c47f1ddce3d88c7217ae248438c34a5026 -size 858 +oid sha256:92cb29d99646955e70bcd07b0061471c3168dd471f6d0280bebfbb48f1b5dedc +size 957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png index bca146111..7aeb788d6 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db9d2722cb695c4913c07ef49fd1ea38d5567493f676982c9605b4021516a530 -size 996 +oid sha256:45a3ccced32ad9e31239a882386973d4123d69934627b47096ea1b9450c63775 +size 1083 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png index bf0d01419..c2056aacb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7eb139670ddcae46d9e57107bf5f2e5e79befcbbafbdc61d9a5998f06c6c82da -size 837 +oid sha256:6320f82d43ac4c8474cba384cae43217c2abaec77762d754d4c0d13465b6743b +size 869 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png index 2cc18734a..c2056aacb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8f2674e83b193e6b9957ed1c9ce07bd597a0b9d6e06451545a108a0d0908173 -size 2361 +oid sha256:6320f82d43ac4c8474cba384cae43217c2abaec77762d754d4c0d13465b6743b +size 869 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png index 2cc18734a..c2056aacb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8f2674e83b193e6b9957ed1c9ce07bd597a0b9d6e06451545a108a0d0908173 -size 2361 +oid sha256:6320f82d43ac4c8474cba384cae43217c2abaec77762d754d4c0d13465b6743b +size 869 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png index d0817de1e..b4c6a8bb5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e71fa6672efffc6bc3a98b5fc18c6b8a01c93025183be6e62d85ac725470d39a -size 3000 +oid sha256:7ad6725d8ebb7f255855e600a9036d4649a844e7bc6ca6e0993c4afac75fe056 +size 2932 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png index 9077c5320..a37448cd3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77fec86277b861b6909c6d1ae53903fe342d578ee19287763b9df264e596ed70 -size 7692 +oid sha256:a7aa551c29f2eef2434f878c1241e374b74cda4347b41c8899e597e12a4a78ec +size 3323 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png index 9077c5320..a37448cd3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77fec86277b861b6909c6d1ae53903fe342d578ee19287763b9df264e596ed70 -size 7692 +oid sha256:a7aa551c29f2eef2434f878c1241e374b74cda4347b41c8899e597e12a4a78ec +size 3323 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png index 2607856b9..332e8d803 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b -size 3141 +oid sha256:05eccdd3499749dbf54338e0f72890484a9f4412ec98f5eebed1073e951f57f1 +size 3135 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png index 2607856b9..332e8d803 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b -size 3141 +oid sha256:05eccdd3499749dbf54338e0f72890484a9f4412ec98f5eebed1073e951f57f1 +size 3135 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png index 2607856b9..332e8d803 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c0e6adea3a6affd6c2cdf5f1ed75cbc1f053a1b63df872bac2b9e9909cfc1b -size 3141 +oid sha256:05eccdd3499749dbf54338e0f72890484a9f4412ec98f5eebed1073e951f57f1 +size 3135 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png index 3e34931c8..a4b78daf4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bc756a96d67fc395b5d87351a72802cccee088ee7a59f6bb13cb4a9b7487c -size 8181 +oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e +size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png index 3756914ad..74f9d6ab1 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28272eb411bf50b7903999be32121f6c498598899af3dd6517d5aeefb0b3371f -size 3095 +oid sha256:72cb6df264e1b343af92dfecb0585ae6b4b0613973b8b4e6e03b0fb69457bd7a +size 3039 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png index 239a2e5ad..a04491b19 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5771531a0e424de19edb9b59c759df3a3ac5d769d4e5495a67179590f3fe8bd9 -size 2038 +oid sha256:74613b0a8e9b7ca778876526d878214eba77dfcb61dab80f84dbf171bd9ed5b8 +size 1963 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png index 6ffabd4f5..865cef7dc 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:305b538606ca3005522be3b51e76128503427dd21a212ef59797e6d26553ec31 -size 36499 +oid sha256:280935e7896f0da062ab8ec4c2455f09f95d88728b4cd28a697a942841002d7c +size 36452 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png index 36b58fc84..11493e17c 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524e3d84b5257ddbacbab1b022de0a2f98d326915e5650bb199156ecd85ac5b8 -size 12961 +oid sha256:3c2b561014aa035424a8745a1c20de3ce33efb5d11137b065a9ec43eed279dd8 +size 12884 diff --git a/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd b/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd deleted file mode 100644 index 98b5eb6f2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd +++ /dev/null @@ -1 +0,0 @@ -optipng.exe -o 7 ./**/*.png \ No newline at end of file diff --git a/tests/Images/ReferenceOutput/Drawing/optipng.exe b/tests/Images/ReferenceOutput/Drawing/optipng.exe deleted file mode 100644 index 49f9dee097ba1bca0639412cfd0de6f7078f8275..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103424 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¥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 WA)lYjPee=e3!cLW{(t=^4E!5_eP?3; diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index 88aa5c7f2..b4548df44 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc4d11f391889b6491624ec21edcf0928589fdc19bb81789ebb2e8ef1de62f73 -size 190 +oid sha256:d7f7aa753d9d88fe0506a9e5f871ec6a1c734585e5591216cfd432de13eb89e8 +size 240 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png deleted file mode 100644 index 6984eb70f..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2e05a237bfd4f5ce3083f3978cedff7c67d4d25ca4a4b2a2edc58d0a46d9444 -size 1988 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png deleted file mode 100644 index 4eb87dfd1..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b471b131f89b3c45c9dbe9a3cc421e00de41f4143890cb84060c86793184b024 -size 2214 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png deleted file mode 100644 index 9c07514e6..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7c56684bdc6fe4b3cc6b5acfcc400adf4ddd801ca06b587eb7689eb2e4bb857d -size 3132 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png deleted file mode 100644 index cc11924f9..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d9dd0363fe1f9d13b8d27abb812fae6651209b1e6ad3fe54d94e967883326c7 -size 3532 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index 88aa5c7f2..29685ad13 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc4d11f391889b6491624ec21edcf0928589fdc19bb81789ebb2e8ef1de62f73 -size 190 +oid sha256:35060ebe93aba15a6a0982ded5a13e6d3b01bc3d22a6cf65498d775aff721c88 +size 262 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index 99d393223..4bd17cf3b 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33b606c57a64b6fb4efedf17ad69d21b877c6ce67d96442cfa89a5807a93e52f -size 1097 +oid sha256:3f5c71327671428fabd8570ac86a823fd723eca816a75106424e1e0b419b49c6 +size 1039 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index e00478bdf..8a1721063 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6b5857f7baa68ded78bd2101bfa1f21efb95109589e71dc692850bb72931b6e -size 1152 +oid sha256:d9e53b3058726c860b17d9672b43300d80bacee1dbc8f38bfc48ca20dd82e74b +size 1100 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index 2fb3f90f0..959e8d24b 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a68b8a9d9f7cecf38b204403607ad1b5b98cfe82efa86addbcd9da3be3e189d2 -size 1130 +oid sha256:8cde7d8a2ecb705e5c727c950368c083ca375588a12e945054d2ece6f91d5e5a +size 1136 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index c1ee69bf7..1bef63da3 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e886fe91f9b2afda61c131cbb7ef9fe00a0cc5cbe039e7fab0565259cd595ea -size 1310 +oid sha256:be1c4e5ad79a9b08a0eb9323406b46b14cffecc2dde635220fbaf8599bb1738c +size 1254 diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 7cca80360..095abbcf4 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d060360cba0713727e6dbab45f30fce0890987fec87933c860b3d459d26f21f3 -size 116720 +oid sha256:38dde98c9995bc7972e2c02c93cb134ef7cb0da093fdd2e24b47b9085dcd1932 +size 116223 diff --git a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png index aac6fb6ed..2f25697cc 100644 --- a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png +++ b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad7ff052941d7371a8403ebf44c916e479b9bcd7e41ca08475b8bde5817b661d -size 8867 +oid sha256:5c54534ff4610093a76df8fd82f23618f080b0aa0718a7b06a18c27861dcc73d +size 8920 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index f174b8178..08cc87986 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0981d21ed8f75ee1fffefdae75505365bea3715bfbf8bda56d278792766f9b09 -size 10939 +oid sha256:cc680ab6ee1d5d28f6b35e959402d0ee28f314a9cfc5df39cb04f234e9b51b8f +size 11082 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index 681c0543a..829180a52 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53206cc3329cb5a49afd48cb17a98a4ced8b38bc6f0b90b5f02e647b0f23e8ee -size 10940 +oid sha256:5ab6a73fbda08c85580c7f326fb53ac3e51aa16ff6c16d95813e4feee29b5fb0 +size 11082 From 6f7e0c668dec2c81f441ca0ca9b7358b6a4c5975 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 20:00:10 +1000 Subject: [PATCH 111/136] GPU: add gradient, pattern & recolor brushes --- README.md | 7 +- .../Shaders/CompositeComputeShader.cs | 293 ++++++++- .../WEBGPU_BACKEND_PROCESS.md | 36 +- .../WebGPUDrawingBackend.cs | 332 +++++++++-- .../Processing/EllipticGradientBrush.cs | 102 ++-- .../Processing/GradientBrush.cs | 68 +-- .../Processing/LinearGradientBrush.cs | 178 +++--- .../Processing/PatternBrush.cs | 5 + .../Processing/RadialGradientBrush.cs | 27 +- .../Processing/SweepGradientBrush.cs | 92 +-- .../Backends/WebGPUDrawingBackendTests.cs | 554 +++++++++++++++++- ...PatternBrushes_MatchesReference_Rgba32.png | 4 +- ...tput_FillPath_EllipticGradient_Default.png | 3 + ...Path_EllipticGradient_WebGPU_CPURegion.png | 3 + ..._EllipticGradient_WebGPU_NativeSurface.png | 3 + ...lPath_EllipticGradient_Reflect_Default.png | 3 + ...ipticGradient_Reflect_WebGPU_CPURegion.png | 3 + ...cGradient_Reflect_WebGPU_NativeSurface.png | 3 + ...Output_FillPath_LinearGradient_Default.png | 3 + ...llPath_LinearGradient_WebGPU_CPURegion.png | 3 + ...th_LinearGradient_WebGPU_NativeSurface.png | 3 + ...FillPath_LinearGradient_Repeat_Default.png | 3 + ...LinearGradient_Repeat_WebGPU_CPURegion.png | 3 + ...arGradient_Repeat_WebGPU_NativeSurface.png | 3 + ...Path_LinearGradient_ThreePoint_Default.png | 3 + ...arGradient_ThreePoint_WebGPU_CPURegion.png | 3 + ...adient_ThreePoint_WebGPU_NativeSurface.png | 3 + ...FillPath_PatternBrush_Diagonal_Default.png | 3 + ...PatternBrush_Diagonal_WebGPU_CPURegion.png | 3 + ...ernBrush_Diagonal_WebGPU_NativeSurface.png | 3 + ...llPath_PatternBrush_Horizontal_Default.png | 3 + ...tternBrush_Horizontal_WebGPU_CPURegion.png | 3 + ...nBrush_Horizontal_WebGPU_NativeSurface.png | 3 + ...FillPath_RadialGradient_Single_Default.png | 3 + ...RadialGradient_Single_WebGPU_CPURegion.png | 3 + ...alGradient_Single_WebGPU_NativeSurface.png | 3 + ...lPath_RadialGradient_TwoCircle_Default.png | 3 + ...ialGradient_TwoCircle_WebGPU_CPURegion.png | 3 + ...radient_TwoCircle_WebGPU_NativeSurface.png | 3 + ...ltOutput_FillPath_RecolorBrush_Default.png | 3 + ...FillPath_RecolorBrush_WebGPU_CPURegion.png | 3 + ...Path_RecolorBrush_WebGPU_NativeSurface.png | 3 + ...tOutput_FillPath_SweepGradient_Default.png | 3 + ...illPath_SweepGradient_WebGPU_CPURegion.png | 3 + ...ath_SweepGradient_WebGPU_NativeSurface.png | 3 + ...lPath_SweepGradient_PartialArc_Default.png | 3 + ...epGradient_PartialArc_WebGPU_CPURegion.png | 3 + ...adient_PartialArc_WebGPU_NativeSurface.png | 3 + 48 files changed, 1474 insertions(+), 332 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png diff --git a/README.md b/README.md index ef44ddf34..13bd490c0 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,10 @@ SixLabors.ImageSharp.Drawing [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp.Drawing/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp.Drawing/actions) [![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp.Drawing/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp.Drawing) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE) -[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) -### **ImageSharp.Drawing** provides extensions to ImageSharp containing powerful, Cross-Platform 2D polygon manipulation and drawing APIs. - -Designed to democratize image processing, ImageSharp.Drawing brings you an incredibly powerful yet beautifully simple API. - -Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. +**ImageSharp.Drawing** is a cross-platform 2D drawing library built on top of [ImageSharp](https://github.com/SixLabors/ImageSharp). It provides path construction, polygon manipulation, fills, strokes, gradient brushes, pattern brushes, and text rendering. Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard). ## License diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index e1697a52a..3d310cac3 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -50,14 +50,18 @@ struct Params { color_blend_mode: u32, alpha_composition_mode: u32, blend_percentage: u32, - solid_r: u32, - solid_g: u32, - solid_b: u32, - solid_a: u32, + gp0: u32, + gp1: u32, + gp2: u32, + gp3: u32, rasterization_mode: u32, antialias_threshold: u32, - pad0: u32, - pad1: u32, + gp4: u32, + gp5: u32, + gp6: u32, + gp7: u32, + stops_offset: u32, + stop_count: u32, }; struct DispatchConfig { @@ -87,6 +91,16 @@ struct DispatchConfig { @group(0) @binding(5) var dispatch_config: DispatchConfig; @group(0) @binding(6) var band_offsets: array; + struct ColorStop { + ratio: f32, + r: f32, + g: f32, + b: f32, + a: f32, + }; + + @group(0) @binding(7) var color_stops: array; + // Workgroup shared memory for per-tile coverage accumulation. // Layout: 16 rows x 16 columns. Index = row * 16 + col. var tile_cover: array, 256>; @@ -101,10 +115,208 @@ struct DispatchConfig { const EO_MASK: i32 = 511; const EO_PERIOD: i32 = 512; + // Brush type constants. Must match PreparedBrushType in WebGPUDrawingBackend.cs. + const BRUSH_SOLID: u32 = 0u; + const BRUSH_IMAGE: u32 = 1u; + const BRUSH_LINEAR_GRADIENT: u32 = 2u; + const BRUSH_RADIAL_GRADIENT: u32 = 3u; + const BRUSH_RADIAL_GRADIENT_TWO_CIRCLE: u32 = 4u; + const BRUSH_ELLIPTIC_GRADIENT: u32 = 5u; + const BRUSH_SWEEP_GRADIENT: u32 = 6u; + const BRUSH_PATTERN: u32 = 7u; + const BRUSH_RECOLOR: u32 = 8u; + fn u32_to_f32(bits: u32) -> f32 { return bitcast(bits); } + // Exact copy of C# GradientBrushApplicator.this[x, y] color sampling. + // Combines repetition mode + GetGradientSegment + lerp into one function. + // Returns vec4(0) with alpha=0 for DontFill outside [0,1]. + fn sample_brush_gradient(raw_t: f32, mode: u32, offset: u32, count: u32) -> vec4 { + if count == 0u { return vec4(0.0); } + + var t = raw_t; + + // C# switch (this.repetitionMode) + if mode == 1u { + // Repeat: positionOnCompleteGradient %= 1; + t = t % 1.0; + } else if mode == 2u { + // Reflect: positionOnCompleteGradient %= 2; + // if (positionOnCompleteGradient > 1) { positionOnCompleteGradient = 2 - positionOnCompleteGradient; } + t = t % 2.0; + if t > 1.0 { t = 2.0 - t; } + } else if mode == 3u { + // DontFill: if (positionOnCompleteGradient is > 1 or < 0) { return Transparent; } + if t < 0.0 || t > 1.0 { return vec4(0.0); } + } + // mode 0 (None): do nothing + + if count == 1u { + let s = color_stops[offset]; + return vec4(s.r, s.g, s.b, s.a); + } + + // C# GetGradientSegment + // ColorStop localGradientFrom = this.colorStops[0]; + // ColorStop localGradientTo = default; + // foreach (ColorStop colorStop in this.colorStops) + // { + // localGradientTo = colorStop; + // if (colorStop.Ratio > positionOnCompleteGradient) { break; } + // localGradientFrom = localGradientTo; + // } + var from_idx = 0u; + var to_idx = 0u; + for (var i = 0u; i < count; i++) { + to_idx = i; + if color_stops[offset + i].ratio > t { + break; + } + from_idx = i; + } + + let from_stop = color_stops[offset + from_idx]; + let to_stop = color_stops[offset + to_idx]; + + // C#: if (from.Color.Equals(to.Color)) { return from.Color.ToPixel(); } + let from_color = vec4(from_stop.r, from_stop.g, from_stop.b, from_stop.a); + let to_color = vec4(to_stop.r, to_stop.g, to_stop.b, to_stop.a); + if all(from_color == to_color) { + return from_color; + } + + // C#: float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); + let range = to_stop.ratio - from_stop.ratio; + let local_t = (t - from_stop.ratio) / range; + + // C#: Vector4.Lerp(from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), onLocalGradient) + return mix(from_color, to_color, local_t); + } + + // Linear gradient: project pixel onto gradient axis. + fn linear_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { + let start_x = u32_to_f32(cmd.gp0); + let start_y = u32_to_f32(cmd.gp1); + let end_x = u32_to_f32(cmd.gp2); + let end_y = u32_to_f32(cmd.gp3); + let along_x = end_x - start_x; + let along_y = end_y - start_y; + let along_sq = along_x * along_x + along_y * along_y; + if along_sq < 1e-12 { return 0.0; } + let dx = x - start_x; + let dy = y - start_y; + return (dx * along_x + dy * along_y) / along_sq; + } + + // Single-circle radial gradient. + // gp0=cx, gp1=cy, gp2=radius, gp3=repetition_mode + fn radial_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { + let cx = u32_to_f32(cmd.gp0); + let cy = u32_to_f32(cmd.gp1); + let radius = u32_to_f32(cmd.gp2); + if radius < 1e-20 { return 0.0; } + return length(vec2(x - cx, y - cy)) / radius; + } + + // Two-circle radial gradient. + // gp0=c0.x, gp1=c0.y, gp2=c1.x, gp3=c1.y, gp4=r0, gp5=r1 + fn radial_gradient_two_t(x: f32, y: f32, cmd: Params) -> f32 { + let c0x = u32_to_f32(cmd.gp0); + let c0y = u32_to_f32(cmd.gp1); + let c1x = u32_to_f32(cmd.gp2); + let c1y = u32_to_f32(cmd.gp3); + let r0 = u32_to_f32(cmd.gp4); + let r1 = u32_to_f32(cmd.gp5); + + let dx_c = c1x - c0x; + let dy_c = c1y - c0y; + let dr = r1 - r0; + let dd = dx_c * dx_c + dy_c * dy_c; + let denom = dd - dr * dr; + + let qx = x - c0x; + let qy = y - c0y; + + // Concentric case (centers equal) or degenerate (denom == 0). + if dd < 1e-10 || abs(denom) < 1e-10 { + let dist = length(vec2(qx, qy)); + let abs_dr = max(abs(dr), 1e-20); + return (dist - r0) / abs_dr; + } + + // General case: t = (q·d - r0*dr) / denom. + let num = qx * dx_c + qy * dy_c - r0 * dr; + return num / denom; + } + + // Elliptic gradient. Computes rotation and radii from raw brush properties. + // gp0=center.x, gp1=center.y, gp2=refEnd.x, gp3=refEnd.y, gp4=axisRatio + fn elliptic_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { + let cx = u32_to_f32(cmd.gp0); + let cy = u32_to_f32(cmd.gp1); + let ref_x = u32_to_f32(cmd.gp2); + let ref_y = u32_to_f32(cmd.gp3); + let axis_ratio = u32_to_f32(cmd.gp4); + + let ref_dx = ref_x - cx; + let ref_dy = ref_y - cy; + let rotation = atan2(ref_dy, ref_dx); + let cos_r = cos(rotation); + let sin_r = sin(rotation); + let rx_sq = ref_dx * ref_dx + ref_dy * ref_dy; + let ry_sq = rx_sq * axis_ratio * axis_ratio; + + let px = x - cx; + let py = y - cy; + let rotated_x = px * cos_r - py * sin_r; + let rotated_y = px * sin_r + py * cos_r; + + if rx_sq < 1e-20 { return 0.0; } + if ry_sq < 1e-20 { return 0.0; } + return rotated_x * rotated_x / rx_sq + rotated_y * rotated_y / ry_sq; + } + + // Sweep (angular) gradient. Computes radians and sweep from raw degrees. + // gp0=center.x, gp1=center.y, gp2=startAngleDegrees, gp3=endAngleDegrees + fn sweep_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { + let cx = u32_to_f32(cmd.gp0); + let cy = u32_to_f32(cmd.gp1); + let start_deg = u32_to_f32(cmd.gp2); + let end_deg = u32_to_f32(cmd.gp3); + + let start_rad = start_deg * 0.017453292; // PI / 180 + let end_rad = end_deg * 0.017453292; + + // Compute sweep, normalizing to (0, 2PI]. + var sweep = (end_rad - start_rad) % 6.283185307; + if sweep <= 0.0 { sweep += 6.283185307; } + if abs(sweep) < 1e-6 { sweep = 6.283185307; } + let is_full = abs(sweep - 6.283185307) < 1e-6; + let inv_sweep = 1.0 / sweep; + + let dx = x - cx; + let dy = y - cy; + + // atan2(-dy, dx) gives clockwise angles in y-down space. + var angle = atan2(-dy, dx); + if angle < 0.0 { angle += 6.283185307; } + + // Rotate basis by 180 degrees. + angle += 3.141592653; + if angle >= 6.283185307 { angle -= 6.283185307; } + + // Phase measured clockwise from start. + var phase = angle - start_rad; + if phase < 0.0 { phase += 6.283185307; } + + if is_full { + return phase / 6.283185307; + } + return phase * inv_sweep; + } + __DECODE_TEXEL_FUNCTION__ __ENCODE_OUTPUT_FUNCTION__ @@ -952,12 +1164,12 @@ fn cs_main( let effective_coverage = coverage_value * blend_percentage; var brush = vec4( - u32_to_f32(command.solid_r), - u32_to_f32(command.solid_g), - u32_to_f32(command.solid_b), - u32_to_f32(command.solid_a)); + u32_to_f32(command.gp0), + u32_to_f32(command.gp1), + u32_to_f32(command.gp2), + u32_to_f32(command.gp3)); - if command.brush_type == 1u { + if command.brush_type == BRUSH_IMAGE { let origin_x = bitcast(command.brush_origin_x); let origin_y = bitcast(command.brush_origin_y); let region_x = i32(command.brush_region_x); @@ -967,6 +1179,65 @@ fn cs_main( let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x; let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y; brush = __LOAD_BRUSH__; + } else if command.brush_type == BRUSH_LINEAR_GRADIENT { + let px = f32(source_x) + 0.5; + let py = f32(source_y) + 0.5; + let raw_t = linear_gradient_t(px, py, command); + brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count); + } else if command.brush_type == BRUSH_RADIAL_GRADIENT { + let px = f32(source_x) + 0.5; + let py = f32(source_y) + 0.5; + let raw_t = radial_gradient_t(px, py, command); + brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count); + } else if command.brush_type == BRUSH_RADIAL_GRADIENT_TWO_CIRCLE { + let px = f32(source_x) + 0.5; + let py = f32(source_y) + 0.5; + let raw_t = radial_gradient_two_t(px, py, command); + brush = sample_brush_gradient(raw_t, command.gp6, command.stops_offset, command.stop_count); + } else if command.brush_type == BRUSH_ELLIPTIC_GRADIENT { + let px = f32(source_x) + 0.5; + let py = f32(source_y) + 0.5; + let raw_t = elliptic_gradient_t(px, py, command); + brush = sample_brush_gradient(raw_t, command.gp5, command.stops_offset, command.stop_count); + } else if command.brush_type == BRUSH_SWEEP_GRADIENT { + let px = f32(source_x) + 0.5; + let py = f32(source_y) + 0.5; + let raw_t = sweep_gradient_t(px, py, command); + brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count); + } else if command.brush_type == BRUSH_PATTERN { + let pw = u32_to_f32(command.gp0); + let ph = u32_to_f32(command.gp1); + let ox = u32_to_f32(command.gp2); + let oy = u32_to_f32(command.gp3); + let fx = f32(source_x) - ox; + let fy = f32(source_y) - oy; + let pw_i = i32(pw); + let ph_i = i32(ph); + let pxi = ((i32(fx) % pw_i) + pw_i) % pw_i; + let pyi = ((i32(fy) % ph_i) + ph_i) % ph_i; + let idx = command.stops_offset + u32(pyi) * u32(pw_i) + u32(pxi); + let c = color_stops[idx]; + brush = vec4(c.r, c.g, c.b, c.a); + } else if command.brush_type == BRUSH_RECOLOR { + let src_r = u32_to_f32(command.gp0); + let src_g = u32_to_f32(command.gp1); + let src_b = u32_to_f32(command.gp2); + let src_a = u32_to_f32(command.gp3); + let tgt_r = u32_to_f32(command.gp4); + let tgt_g = u32_to_f32(command.gp5); + let tgt_b = u32_to_f32(command.gp6); + let tgt_a = u32_to_f32(command.gp7); + let threshold = bitcast(command.stops_offset); + let dr = destination.r - src_r; + let dg = destination.g - src_g; + let db = destination.b - src_b; + let da = destination.a - src_a; + let dist_sq = dr * dr + dg * dg + db * db + da * da; + if dist_sq <= threshold * threshold { + brush = vec4(tgt_r, tgt_g, tgt_b, tgt_a); + } else { + brush = destination; + } } let src = vec4(brush.rgb, brush.a * effective_coverage); diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 823bc49c5..026f6e380 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -54,7 +54,7 @@ DrawingCanvasBatcher.Flush() -> barrier, then each thread accumulates its coverage from shared memory -> applies fill rule (non-zero or even-odd) -> if aliased mode: snaps coverage to binary using antialias threshold - -> samples brush (solid color or image texture) + -> samples brush (see Brush Types below) -> composes pixel using Porter-Duff alpha composition + color blend mode -> writes final pixel to output texture -> destination writeback: @@ -106,17 +106,37 @@ Each edge is a 32-byte `GpuEdge` struct (sequential layout): ### CSR Buffers -- `csr-offsets`: `array` — per-band prefix sum. `offsets[band]..offsets[band+1]` gives the range of edge indices for that 16-row band. -- `csr-indices`: `array` — edge indices within each band, ordered by band. +- `csr-offsets`: `array` - per-band prefix sum. `offsets[band]..offsets[band+1]` gives the range of edge indices for that 16-row band. +- `csr-indices`: `array` - edge indices within each band, ordered by band. ### Command Parameters -Each `PreparedCompositeParameters` struct (26 × u32 = 104 bytes) contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, blend percentage, rasterization mode (0 = antialiased, 1 = aliased), and antialias threshold (float as u32 bitcast). +Each `PreparedCompositeParameters` struct (32 × u32 = 128 bytes) contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush type and configuration (gp0–gp7), blend/composition mode, blend percentage, rasterization mode (0 = antialiased, 1 = aliased), antialias threshold (float as u32 bitcast), and color stop buffer references (stops_offset, stop_count). ### Dispatch Config `PreparedCompositeDispatchConfig` contains target dimensions, tile counts, source/output origins, and command count. +## Brush Types + +All brush types are handled natively on the GPU. The backend passes raw brush properties via gp0–gp7 fields; derived values (trig, distances, etc.) are computed per-pixel in the shader. + +| Constant | Type | gp fields | +|---|---|---| +| `BRUSH_SOLID` (0) | Solid color | gp0–3 = RGBA | +| `BRUSH_IMAGE` (1) | Image texture | brush_origin/region fields + texture binding | +| `BRUSH_LINEAR_GRADIENT` (2) | Linear gradient | gp0–3 = start.xy, end.xy; gp4 = repetition mode | +| `BRUSH_RADIAL_GRADIENT` (3) | Radial (single circle) | gp0–1 = center; gp2 = radius; gp4 = repetition mode | +| `BRUSH_RADIAL_GRADIENT_TWO_CIRCLE` (4) | Radial (two circle) | gp0–3 = c0.xy, c1.xy; gp4–5 = r0, r1; gp6 = repetition mode | +| `BRUSH_ELLIPTIC_GRADIENT` (5) | Elliptic gradient | gp0–3 = center.xy, refEnd.xy; gp4 = axis ratio; gp5 = repetition mode | +| `BRUSH_SWEEP_GRADIENT` (6) | Sweep gradient | gp0–3 = center.xy, startAngleDeg, endAngleDeg; gp4 = repetition mode | +| `BRUSH_PATTERN` (7) | Pattern | gp0–1 = width, height; gp2–3 = origin; cells packed in color stops buffer | +| `BRUSH_RECOLOR` (8) | Recolor | gp0–3 = source RGBA; gp4–7 = target RGBA; stops_offset = threshold | + +Gradient color stops are packed into a shared storage buffer (binding 7). Each stop is 5 × f32 (ratio, R, G, B, A). The `stops_offset` and `stop_count` fields in the command parameters index into this buffer. Color stop interpolation in the shader is an exact copy of the C# `GradientBrushApplicator.GetGradientSegment` + lerp logic - including unclamped `t` values, stable sort order, and per-mode repetition semantics. + +Color stops are sorted by ratio in the `GradientBrush` constructor using a stable insertion sort to preserve the order of equal-ratio stops. + ## Shader Bindings (CompositeComputeShader) | Binding | Type | Description | @@ -127,8 +147,8 @@ Each `PreparedCompositeParameters` struct (26 × u32 = 104 bytes) contains desti | 3 | `texture_storage_2d, write` | Output texture | | 4 | `storage, read` | Command parameters (`array`) | | 5 | `uniform` | Dispatch config | -| 6 | `storage, read` | CSR offsets (`array`) | -| 7 | `storage, read` | CSR indices (`array`) | +| 6 | `storage, read` | Band offsets (`array`) | +| 7 | `storage, read` | Color stops / pattern buffer (`array`) | ## Context and Resource Lifetime @@ -150,7 +170,7 @@ Each `PreparedCompositeParameters` struct (26 × u32 = 104 bytes) contains desti Fallback is scene-scoped and triggered when: - The pixel format has no supported WebGPU texture format mapping. -- Any command uses an unsupported brush type (only `SolidBrush` and `ImageBrush` are GPU-composable). +- Any command uses an unsupported brush type (`PathGradientBrush` is not GPU-composable; all other brush types are supported). - Any GPU operation fails during the flush. Fallback path: @@ -171,7 +191,7 @@ Coverage rasterization and compositing are fused into a single compute dispatch. Edge preparation (path flattening, fixed-point conversion, CSR construction) runs on the CPU. The `path.Flatten()` cost is shared with the CPU rasterizer pipeline. CSR construction is three passes over the edge set: count, prefix sum, scatter. -Both the CPU and GPU backends use per-band parallel stroke expansion — the CPU +Both the CPU and GPU backends use per-band parallel stroke expansion - the CPU via `DefaultRasterizer.RasterizeStrokeRows` and the GPU via `StrokeExpandComputeShader`. Both share the same `StrokeEdgeFlags` enum and `DashPathSplitter` (in the core project). The CPU backend fuses stroke expansion diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index e7f897d32..c497538ef 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -37,10 +37,10 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp { private const int CompositeTileWidth = 16; private const int CompositeTileHeight = 16; - private const uint PreparedBrushTypeSolid = 0; - private const uint PreparedBrushTypeImage = 1; + private const string PreparedCompositeParamsBufferKey = "prepared-composite/params"; private const string PreparedCompositeDispatchConfigBufferKey = "prepared-composite/dispatch-config"; + private const string PreparedCompositeColorStopsBufferKey = "prepared-composite/color-stops"; private const string StrokeExpandPipelineKey = "stroke-expand"; private const string StrokeExpandCommandsBufferKey = "stroke-expand/commands"; private const string StrokeExpandConfigBufferKey = "stroke-expand/config"; @@ -59,6 +59,22 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp public WebGPUDrawingBackend() => this.fallbackBackend = DefaultDrawingBackend.Instance; + /// + /// GPU brush type identifiers. Values match the WGSL brush_type field constants. + /// + private enum PreparedBrushType : uint + { + Solid = 0, + Image = 1, + LinearGradient = 2, + RadialGradient = 3, + RadialGradientTwoCircle = 4, + EllipticGradient = 5, + SweepGradient = 6, + Pattern = 7, + Recolor = 8, + } + /// /// Gets the testing-only diagnostic counter for total coverage preparation requests. /// @@ -307,7 +323,15 @@ private static bool AreAllCompositionBrushesSupported(IReadOnlyList [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSupportedCompositionBrush(Brush brush) => brush is SolidBrush or ImageBrush; + private static bool IsSupportedCompositionBrush(Brush brush) + => brush is SolidBrush + or ImageBrush + or LinearGradientBrush + or RadialGradientBrush + or EllipticGradientBrush + or SweepGradientBrush + or PatternBrush + or RecolorBrush; /// /// Executes the scene on the CPU fallback backend. @@ -698,6 +722,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out uint parameterSize = (uint)Unsafe.SizeOf(); IMemoryOwner parametersOwner = flushContext.MemoryAllocator.Allocate(commandCount); + List colorStopsList = []; try { int flushCommandCount = commandCount; @@ -720,7 +745,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out { PreparedCompositionCommand command = commands[i]; - uint brushType; + PreparedBrushType brushType; int brushOriginX = 0; int brushOriginY = 0; int brushRegionX = 0; @@ -728,15 +753,17 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out int brushRegionWidth = 1; int brushRegionHeight = 1; Vector4 solidColor = default; + uint gp4 = 0, gp5 = 0, gp6 = 0, gp7 = 0; + uint stopsOffset = 0, stopCount = 0; if (command.Brush is SolidBrush solidBrush) { - brushType = PreparedBrushTypeSolid; + brushType = PreparedBrushType.Solid; solidColor = solidBrush.Color.ToScaledVector4(); } else if (command.Brush is ImageBrush imageBrush) { - brushType = PreparedBrushTypeImage; + brushType = PreparedBrushType.Image; Image image = (Image)imageBrush.SourceImage; if (!TryGetOrCreateImageTextureView( @@ -769,6 +796,77 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out brushOriginX = command.BrushBounds.X + imageBrush.Offset.X - targetBounds.X - targetLocalBounds.X; brushOriginY = command.BrushBounds.Y + imageBrush.Offset.Y - targetBounds.Y - targetLocalBounds.Y; } + else if (command.Brush is LinearGradientBrush linearBrush) + { + brushType = PreparedBrushType.LinearGradient; + PointF start = linearBrush.StartPoint; + PointF end = linearBrush.EndPoint; + + solidColor = new Vector4(start.X, start.Y, end.X, end.Y); + gp4 = (uint)linearBrush.RepetitionMode; + PackColorStops(linearBrush.ColorStops, colorStopsList, out stopsOffset, out stopCount); + } + else if (command.Brush is RadialGradientBrush radialBrush) + { + if (radialBrush.IsTwoCircle) + { + brushType = PreparedBrushType.RadialGradientTwoCircle; + + // Pass raw brush properties; shader computes derived values. + solidColor = new Vector4(radialBrush.Center0.X, radialBrush.Center0.Y, radialBrush.Center1!.Value.X, radialBrush.Center1.Value.Y); + gp4 = FloatToUInt32Bits(radialBrush.Radius0); + gp5 = FloatToUInt32Bits(radialBrush.Radius1!.Value); + gp6 = (uint)radialBrush.RepetitionMode; + } + else + { + brushType = PreparedBrushType.RadialGradient; + + // Pass raw brush properties; shader computes derived values. + solidColor = new Vector4(radialBrush.Center0.X, radialBrush.Center0.Y, radialBrush.Radius0, 0f); + gp4 = (uint)radialBrush.RepetitionMode; + } + + PackColorStops(radialBrush.ColorStops, colorStopsList, out stopsOffset, out stopCount); + } + else if (command.Brush is EllipticGradientBrush ellipticBrush) + { + brushType = PreparedBrushType.EllipticGradient; + + // Pass raw brush properties; shader computes rotation and radii. + solidColor = new Vector4(ellipticBrush.Center.X, ellipticBrush.Center.Y, ellipticBrush.ReferenceAxisEnd.X, ellipticBrush.ReferenceAxisEnd.Y); + gp4 = FloatToUInt32Bits(ellipticBrush.AxisRatio); + gp5 = (uint)ellipticBrush.RepetitionMode; + PackColorStops(ellipticBrush.ColorStops, colorStopsList, out stopsOffset, out stopCount); + } + else if (command.Brush is SweepGradientBrush sweepBrush) + { + brushType = PreparedBrushType.SweepGradient; + + // Pass raw brush properties; shader computes radians and sweep. + solidColor = new Vector4(sweepBrush.Center.X, sweepBrush.Center.Y, sweepBrush.StartAngleDegrees, sweepBrush.EndAngleDegrees); + gp4 = (uint)sweepBrush.RepetitionMode; + PackColorStops(sweepBrush.ColorStops, colorStopsList, out stopsOffset, out stopCount); + } + else if (command.Brush is PatternBrush patternBrush) + { + brushType = PreparedBrushType.Pattern; + DenseMatrix pattern = patternBrush.Pattern; + solidColor = new Vector4(pattern.Columns, pattern.Rows, 0f, 0f); + PackPatternColors(pattern, colorStopsList, out stopsOffset); + } + else if (command.Brush is RecolorBrush recolorBrush) + { + brushType = PreparedBrushType.Recolor; + Vector4 src = recolorBrush.SourceColor.ToScaledVector4(); + Vector4 tgt = recolorBrush.TargetColor.ToScaledVector4(); + solidColor = src; + gp4 = FloatToUInt32Bits(tgt.X); + gp5 = FloatToUInt32Bits(tgt.Y); + gp6 = FloatToUInt32Bits(tgt.Z); + gp7 = FloatToUInt32Bits(tgt.W); + stopsOffset = FloatToUInt32Bits(recolorBrush.Threshold); + } else { error = "Unsupported brush type."; @@ -788,29 +886,35 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out int edgeOriginY = destinationY - sourceOffset.Y; PreparedCompositeParameters commandParameters = new( - destinationX, - destinationY, - destinationRegion.Width, - destinationRegion.Height, - edgePlacement.EdgeStart, - edgePlacement.FillRule, - edgeOriginX, - edgeOriginY, - edgePlacement.CsrOffsetsStart, - edgePlacement.CsrBandCount, - brushType, - brushOriginX, - brushOriginY, - brushRegionX, - brushRegionY, - brushRegionWidth, - brushRegionHeight, - (uint)command.GraphicsOptions.ColorBlendingMode, - (uint)command.GraphicsOptions.AlphaCompositionMode, - command.GraphicsOptions.BlendPercentage, - solidColor, - command.GraphicsOptions.Antialias ? 0u : 1u, - command.GraphicsOptions.AntialiasThreshold); + destinationX, + destinationY, + destinationRegion.Width, + destinationRegion.Height, + edgePlacement.EdgeStart, + edgePlacement.FillRule, + edgeOriginX, + edgeOriginY, + edgePlacement.CsrOffsetsStart, + edgePlacement.CsrBandCount, + brushType, + brushOriginX, + brushOriginY, + brushRegionX, + brushRegionY, + brushRegionWidth, + brushRegionHeight, + (uint)command.GraphicsOptions.ColorBlendingMode, + (uint)command.GraphicsOptions.AlphaCompositionMode, + command.GraphicsOptions.BlendPercentage, + solidColor, + command.GraphicsOptions.Antialias ? 0u : 1u, + command.GraphicsOptions.AntialiasThreshold, + gp4, + gp5, + gp6, + gp7, + stopsOffset, + stopCount); parameters[commandIndex] = commandParameters; commandIndex++; @@ -875,9 +979,38 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out &dispatchConfig, dispatchConfigSize); + // Color stops / pattern buffer (binding 7). + nuint colorStopsBufferSize = colorStopsList.Count > 0 + ? (nuint)(colorStopsList.Count * sizeof(float)) + : 20; // minimum 1 ColorStop (5 × f32 = 20 bytes) + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + PreparedCompositeColorStopsBufferKey, + BufferUsage.Storage | BufferUsage.CopyDst, + colorStopsBufferSize, + out WgpuBuffer* colorStopsBuffer, + out _, + out error)) + { + return false; + } + + if (colorStopsList.Count > 0) + { + Span stopsSpan = CollectionsMarshal.AsSpan(colorStopsList); + fixed (float* stopsPtr = stopsSpan) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + colorStopsBuffer, + 0, + stopsPtr, + (nuint)(stopsSpan.Length * sizeof(float))); + } + } + // Band offsets are pre-computed on CPU and uploaded directly. // Edges are pre-split at band boundaries, eliminating CSR index indirection. - BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[7]; + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[8]; bindGroupEntries[0] = new BindGroupEntry { Binding = 0, @@ -921,11 +1054,18 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out Offset = 0, Size = bandOffsetsBufferSize }; + bindGroupEntries[7] = new BindGroupEntry + { + Binding = 7, + Buffer = colorStopsBuffer, + Offset = 0, + Size = colorStopsBufferSize + }; BindGroupDescriptor bindGroupDescriptor = new() { Layout = bindGroupLayout, - EntryCount = 7, + EntryCount = 8, Entries = bindGroupEntries }; @@ -991,7 +1131,7 @@ private bool TryDispatchStrokeExpand( List commands = expandInfo.Commands!; // Create or get the pipeline. - bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + static bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) => TryCreateStrokeExpandBindGroupLayout(api, device, out layout, out layoutError); if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( @@ -1296,7 +1436,7 @@ private static bool TryCreateCompositeBindGroupLayout( out BindGroupLayout* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; entries[0] = new BindGroupLayoutEntry { Binding = 0, @@ -1374,10 +1514,21 @@ private static bool TryCreateCompositeBindGroupLayout( MinBindingSize = 0 } }; + entries[7] = new BindGroupLayoutEntry + { + Binding = 7, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.ReadOnlyStorage, + HasDynamicOffset = false, + MinBindingSize = 0 + } + }; BindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, + EntryCount = 8, Entries = entries }; @@ -1669,6 +1820,52 @@ private static uint DivideRoundUp(int value, int divisor) private static uint FloatToUInt32Bits(float value) => unchecked((uint)BitConverter.SingleToInt32Bits(value)); + /// + /// Packs color stops into the shared float list for GPU upload. + /// Each stop is 5 floats: ratio, R, G, B, A (matching the WGSL ColorStop struct). + /// + private static void PackColorStops( + ReadOnlySpan stops, + List buffer, + out uint offset, + out uint count) + { + offset = (uint)(buffer.Count / 5); + count = (uint)stops.Length; + for (int i = 0; i < stops.Length; i++) + { + ColorStop stop = stops[i]; + Vector4 color = stop.Color.ToScaledVector4(); + buffer.Add(stop.Ratio); + buffer.Add(color.X); + buffer.Add(color.Y); + buffer.Add(color.Z); + buffer.Add(color.W); + } + } + + /// + /// Packs pattern colors into the shared float list for GPU upload. + /// Each cell is 5 floats: ratio (0), R, G, B, A (reusing the ColorStop layout). + /// + private static void PackPatternColors( + DenseMatrix pattern, + List buffer, + out uint offset) + { + offset = (uint)(buffer.Count / 5); + ReadOnlySpan data = pattern.Data; + for (int i = 0; i < data.Length; i++) + { + Vector4 color = data[i].ToScaledVector4(); + buffer.Add(0f); // ratio unused + buffer.Add(color.X); + buffer.Add(color.Y); + buffer.Add(color.Z); + buffer.Add(color.W); + } + } + /// /// Finalizes one flush by submitting command buffers and optionally reading results back to CPU memory. /// @@ -2080,7 +2277,7 @@ public PreparedCompositeDispatchConfig( /// /// Prepared composite command parameters consumed by . - /// Layout matches the WGSL Params struct exactly (26 u32 fields = 104 bytes). + /// Layout matches the WGSL Params struct exactly (32 u32 fields = 128 bytes). /// [StructLayout(LayoutKind.Sequential)] private readonly struct PreparedCompositeParameters @@ -2105,14 +2302,39 @@ private readonly struct PreparedCompositeParameters public readonly uint ColorBlendMode; public readonly uint AlphaCompositionMode; public readonly uint BlendPercentage; - public readonly uint SolidR; - public readonly uint SolidG; - public readonly uint SolidB; - public readonly uint SolidA; + + /// General-purpose brush param 0. For solid brush: R. For gradients: see plan. + public readonly uint Gp0; + + /// General-purpose brush param 1. For solid brush: G. + public readonly uint Gp1; + + /// General-purpose brush param 2. For solid brush: B. + public readonly uint Gp2; + + /// General-purpose brush param 3. For solid brush: A. + public readonly uint Gp3; + public readonly uint RasterizationMode; public readonly uint AntialiasThreshold; - public readonly uint Pad0; - public readonly uint Pad1; + + /// General-purpose brush param 4. + public readonly uint Gp4; + + /// General-purpose brush param 5. + public readonly uint Gp5; + + /// General-purpose brush param 6. + public readonly uint Gp6; + + /// General-purpose brush param 7. + public readonly uint Gp7; + + /// Index into the color stop / pattern buffer. + public readonly uint StopsOffset; + + /// Number of color stops for gradient commands. + public readonly uint StopCount; public PreparedCompositeParameters( int destinationX, @@ -2125,7 +2347,7 @@ public PreparedCompositeParameters( int edgeOriginY, uint csrOffsetsStart, uint csrBandCount, - uint brushType, + PreparedBrushType brushType, int brushOriginX, int brushOriginY, int brushRegionX, @@ -2137,7 +2359,13 @@ public PreparedCompositeParameters( float blendPercentage, Vector4 solidColor, uint rasterizationMode, - float antialiasThreshold) + float antialiasThreshold, + uint gp4 = 0, + uint gp5 = 0, + uint gp6 = 0, + uint gp7 = 0, + uint stopsOffset = 0, + uint stopCount = 0) { this.DestinationX = (uint)destinationX; this.DestinationY = (uint)destinationY; @@ -2149,7 +2377,7 @@ public PreparedCompositeParameters( this.EdgeOriginY = unchecked((uint)edgeOriginY); this.CsrOffsetsStart = csrOffsetsStart; this.CsrBandCount = csrBandCount; - this.BrushType = brushType; + this.BrushType = (uint)brushType; this.BrushOriginX = (uint)brushOriginX; this.BrushOriginY = (uint)brushOriginY; this.BrushRegionX = (uint)brushRegionX; @@ -2159,14 +2387,18 @@ public PreparedCompositeParameters( this.ColorBlendMode = colorBlendMode; this.AlphaCompositionMode = alphaCompositionMode; this.BlendPercentage = FloatToUInt32Bits(blendPercentage); - this.SolidR = FloatToUInt32Bits(solidColor.X); - this.SolidG = FloatToUInt32Bits(solidColor.Y); - this.SolidB = FloatToUInt32Bits(solidColor.Z); - this.SolidA = FloatToUInt32Bits(solidColor.W); + this.Gp0 = FloatToUInt32Bits(solidColor.X); + this.Gp1 = FloatToUInt32Bits(solidColor.Y); + this.Gp2 = FloatToUInt32Bits(solidColor.Z); + this.Gp3 = FloatToUInt32Bits(solidColor.W); this.RasterizationMode = rasterizationMode; this.AntialiasThreshold = FloatToUInt32Bits(antialiasThreshold); - this.Pad0 = 0; - this.Pad1 = 0; + this.Gp4 = gp4; + this.Gp5 = gp5; + this.Gp6 = gp6; + this.Gp7 = gp7; + this.StopsOffset = stopsOffset; + this.StopCount = stopCount; } } } diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index 746f250aa..bed8ac50e 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -14,12 +13,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class EllipticGradientBrush : GradientBrush { - private readonly PointF center; - - private readonly PointF referenceAxisEnd; - - private readonly float axisRatio; - /// /// The center of the elliptical gradient and 0 for the color stops. /// The end point of the reference axis of the ellipse. @@ -38,90 +31,84 @@ public EllipticGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center = center; - this.referenceAxisEnd = referenceAxisEnd; - this.axisRatio = axisRatio; + this.Center = center; + this.ReferenceAxisEnd = referenceAxisEnd; + this.AxisRatio = axisRatio; } + /// + /// Gets the center of the ellipse. + /// + public PointF Center { get; } + + /// + /// Gets the end point of the reference axis. + /// + public PointF ReferenceAxisEnd { get; } + + /// + /// Gets the ratio of the secondary axis to the primary axis. + /// + public float AxisRatio { get; } + /// public override BrushApplicator CreateApplicator( Configuration configuration, GraphicsOptions options, Buffer2DRegion targetRegion, RectangleF region) => - new RadialGradientBrushApplicator( + new EllipticGradientBrushApplicator( configuration, options, targetRegion, - this.center, - this.referenceAxisEnd, - this.axisRatio, - this.ColorStops, + this, + this.ColorStopsArray, this.RepetitionMode); /// - private sealed class RadialGradientBrushApplicator : GradientBrushApplicator + private sealed class EllipticGradientBrushApplicator : GradientBrushApplicator where TPixel : unmanaged, IPixel { private readonly PointF center; - private readonly PointF referenceAxisEnd; - - private readonly float axisRatio; - - private readonly double rotation; - - private readonly float referenceRadius; - - private readonly float secondRadius; - private readonly float cosRotation; private readonly float sinRotation; - private readonly float secondRadiusSquared; - private readonly float referenceRadiusSquared; + private readonly float secondRadiusSquared; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. /// The destination pixel region. - /// Center of the ellipse. - /// Point on one angular points of the ellipse. - /// - /// Ratio of the axis length's. Used to determine the length of the second axis, - /// the first is defined by and . + /// The elliptic gradient brush. /// Definition of colors. /// Defines how the gradient colors are repeated. - public RadialGradientBrushApplicator( + public EllipticGradientBrushApplicator( Configuration configuration, GraphicsOptions options, Buffer2DRegion targetRegion, - PointF center, - PointF referenceAxisEnd, - float axisRatio, + EllipticGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) : base(configuration, options, targetRegion, colorStops, repetitionMode) { - this.center = center; - this.referenceAxisEnd = referenceAxisEnd; - this.axisRatio = axisRatio; - this.rotation = AngleBetween( - this.center, - new PointF(this.center.X + 1, this.center.Y), - this.referenceAxisEnd); - this.referenceRadius = DistanceBetween(this.center, this.referenceAxisEnd); - this.secondRadius = this.referenceRadius * this.axisRatio; - - this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius; - this.secondRadiusSquared = this.secondRadius * this.secondRadius; - - this.sinRotation = (float)Math.Sin(this.rotation); - this.cosRotation = (float)Math.Cos(this.rotation); + this.center = brush.Center; + + float refDx = brush.ReferenceAxisEnd.X - brush.Center.X; + float refDy = brush.ReferenceAxisEnd.Y - brush.Center.Y; + float rotation = MathF.Atan2(refDy, refDx); + float referenceRadius = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondRadius = referenceRadius * brush.AxisRatio; + + this.referenceRadiusSquared = referenceRadius * referenceRadius; + this.secondRadiusSquared = secondRadius * secondRadius; + this.sinRotation = MathF.Sin(rotation); + this.cosRotation = MathF.Cos(rotation); } /// @@ -138,14 +125,5 @@ protected override float PositionOnGradient(float x, float y) return (xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared); } - - private static float AngleBetween(PointF junction, PointF a, PointF b) - { - PointF vA = a - junction; - PointF vB = b - junction; - return MathF.Atan2(vB.Y, vB.X) - MathF.Atan2(vA.Y, vA.X); - } - - private static float DistanceBetween(PointF p1, PointF p2) => Vector2.Distance(p1, p2); } } diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 3d029523c..05c221204 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -18,18 +18,25 @@ public abstract class GradientBrush : Brush protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) { this.RepetitionMode = repetitionMode; - this.ColorStops = colorStops; + + InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio)); + this.ColorStopsArray = colorStops; } /// /// Gets how the colors are repeated beyond the interval [0..1]. /// - protected GradientRepetitionMode RepetitionMode { get; } + public GradientRepetitionMode RepetitionMode { get; } + + /// + /// Gets the color stops for this gradient. + /// + public ReadOnlySpan ColorStops => this.ColorStopsArray; /// - /// Gets the list of color stops for this gradient. + /// Gets the color stops array for use by derived applicators. /// - protected ColorStop[] ColorStops { get; } + protected ColorStop[] ColorStopsArray { get; } /// public override bool Equals(Brush? other) @@ -37,7 +44,7 @@ public override bool Equals(Brush? other) if (other is GradientBrush brush) { return this.RepetitionMode == brush.RepetitionMode - && this.ColorStops?.SequenceEqual(brush.ColorStops) == true; + && this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true; } return false; @@ -45,7 +52,29 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(this.RepetitionMode, this.ColorStops); + => HashCode.Combine(this.RepetitionMode, this.ColorStopsArray); + + /// + /// Sorts the collection in place using a stable insertion sort. + /// is not stable and can reorder + /// equal-ratio color stops, producing non-deterministic gradient results. + /// + private static void InsertionSort(T[] collection, Comparison comparison) + { + int count = collection.Length; + for (int j = 1; j < count; j++) + { + T key = collection[j]; + + int i = j - 1; + for (; i >= 0 && comparison(collection[i], key) > 0; i--) + { + collection[i + 1] = collection[i]; + } + + collection[i + 1] = key; + } + } /// /// Base class for gradient brush applicators @@ -85,9 +114,6 @@ protected GradientBrushApplicator( : base(configuration, options, targetRegion) { this.colorStops = colorStops; - - // Ensure the color-stop order is correct. - InsertionSort(this.colorStops, (x, y) => x.Ratio.CompareTo(y.Ratio)); this.repetitionMode = repetitionMode; this.scanlineWidth = targetRegion.Width; this.allocator = configuration.MemoryAllocator; @@ -229,29 +255,5 @@ protected override void Dispose(bool disposing) return (localGradientFrom, localGradientTo); } - - /// - /// Provides a stable sorting algorithm for the given array. - /// is not stable. - /// - /// The type of element to sort. - /// The array to sort. - /// The comparison delegate. - private static void InsertionSort(T[] collection, Comparison comparison) - { - int count = collection.Length; - for (int j = 1; j < count; j++) - { - T key = collection[j]; - - int i = j - 1; - for (; i >= 0 && comparison(collection[i], key) > 0; i--) - { - collection[i + 1] = collection[i]; - } - - collection[i + 1] = key; - } - } } } diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index a44450329..e8a785be1 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -1,20 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Processing; - using SixLabors.ImageSharp.Memory; +namespace SixLabors.ImageSharp.Drawing.Processing; + /// /// Provides a brush that paints linear gradients within an area. /// Supports both classic two-point gradients and three-point (rotated) gradients. /// public sealed class LinearGradientBrush : GradientBrush { - private readonly PointF startPoint; - private readonly PointF endPoint; - private readonly PointF? rotationPoint; - /// /// Initializes a new instance of the class using /// a start and end point. @@ -30,9 +26,8 @@ public LinearGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.startPoint = p0; - this.endPoint = p1; - this.rotationPoint = null; + this.StartPoint = p0; + this.EndPoint = p1; } /// @@ -54,20 +49,29 @@ public LinearGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.startPoint = p0; - this.endPoint = p1; - this.rotationPoint = rotationPoint; + ResolveAxis(p0, p1, rotationPoint, out PointF start, out PointF end); + this.StartPoint = start; + this.EndPoint = end; } + /// + /// Gets the start point of the gradient axis. + /// + public PointF StartPoint { get; } + + /// + /// Gets the end point of the gradient axis. + /// + public PointF EndPoint { get; } + /// public override bool Equals(Brush? other) { if (other is LinearGradientBrush brush) { return base.Equals(other) - && this.startPoint.Equals(brush.startPoint) - && this.endPoint.Equals(brush.endPoint) - && Nullable.Equals(this.rotationPoint, brush.rotationPoint); + && this.StartPoint.Equals(brush.StartPoint) + && this.EndPoint.Equals(brush.EndPoint); } return false; @@ -75,7 +79,48 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), this.startPoint, this.endPoint, this.rotationPoint); + => HashCode.Combine(base.GetHashCode(), this.StartPoint, this.EndPoint); + + /// + /// Resolves a three-point gradient axis into a two-point axis by projecting + /// the gradient vector (p0→p1) onto the perpendicular of the rotation vector (p0→rotationPoint). + /// This follows the COLRv1 font specification for rotated linear gradients. + /// + /// The gradient start point. + /// The gradient vector endpoint. + /// The rotation reference point. + /// The resolved start point of the gradient axis. + /// The resolved end point of the gradient axis. + private static void ResolveAxis(PointF p0, PointF p1, PointF rotationPoint, out PointF start, out PointF end) + { + // Gradient vector from p0 to p1. + float vx = p1.X - p0.X; + float vy = p1.Y - p0.Y; + + // Rotation vector from p0 to rotation point. + float rx = rotationPoint.X - p0.X; + float ry = rotationPoint.Y - p0.Y; + + // Perpendicular to the rotation vector. + float nx = ry; + float ny = -rx; + + float ndotn = (nx * nx) + (ny * ny); + if (ndotn == 0f) + { + // Degenerate: p0 == rotationPoint, fall back to original axis. + start = p0; + end = p1; + } + else + { + // Project the gradient vector onto the perpendicular direction. + float vdotn = (vx * nx) + (vy * ny); + float scale = vdotn / ndotn; + start = p0; + end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny)); + } + } /// public override BrushApplicator CreateApplicator( @@ -87,10 +132,8 @@ public override BrushApplicator CreateApplicator( configuration, options, targetRegion, - this.startPoint, - this.endPoint, - this.rotationPoint, - this.ColorStops, + this, + this.ColorStopsArray, this.RepetitionMode); /// @@ -101,13 +144,9 @@ private sealed class LinearGradientBrushApplicator : GradientBrushApplic where TPixel : unmanaged, IPixel { private readonly PointF start; - private readonly PointF end; private readonly float alongX; private readonly float alongY; - private readonly float acrossX; - private readonly float acrossY; private readonly float alongsSquared; - private readonly float length; /// /// Initializes a new instance of the class. @@ -115,115 +154,36 @@ private sealed class LinearGradientBrushApplicator : GradientBrushApplic /// The ImageSharp configuration. /// The graphics options. /// The destination pixel region. - /// The gradient start point. - /// The gradient end point. - /// The optional rotation point. + /// The linear gradient brush. /// The gradient color stops. /// Defines how the gradient repeats. public LinearGradientBrushApplicator( Configuration configuration, GraphicsOptions options, Buffer2DRegion targetRegion, - PointF p0, - PointF p1, - PointF? p2, + LinearGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) : base(configuration, options, targetRegion, colorStops, repetitionMode) { - // Determine whether this is a simple linear gradient (2 points) - // or a rotated one (3 points). - if (p2 is null) - { - // Classic SVG-style gradient from start -> end. - this.start = p0; - this.end = p1; - } - else - { - // Compute the rotated gradient axis per COLRv1 rules. - // p0 = start, p1 = gradient vector, p2 = rotation reference. - float vx = p1.X - p0.X; - float vy = p1.Y - p0.Y; - float rx = p2.Value.X - p0.X; - float ry = p2.Value.Y - p0.Y; - - // n = perpendicular to rotation vector - float nx = ry; - float ny = -rx; - - // Avoid divide-by-zero if p0 == p2. - float ndotn = (nx * nx) + (ny * ny); - if (ndotn == 0f) - { - this.start = p0; - this.end = p1; - } - else - { - // Project p1 - p0 onto perpendicular direction. - float vdotn = (vx * nx) + (vy * ny); - float scale = vdotn / ndotn; - - // The derived endpoint after rotation. - this.start = p0; - this.end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny)); - } - } - - // Calculate axis vectors. - this.alongX = this.end.X - this.start.X; - this.alongY = this.end.Y - this.start.Y; + this.start = brush.StartPoint; - // Perpendicular axis vector. - this.acrossX = this.alongY; - this.acrossY = -this.alongX; - - // Precompute squared length and actual length for later use. + this.alongX = brush.EndPoint.X - this.start.X; + this.alongY = brush.EndPoint.Y - this.start.Y; this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY); - this.length = MathF.Sqrt(this.alongsSquared); } /// protected override float PositionOnGradient(float x, float y) { - // Degenerate case: gradient length == 0, use final stop color. if (this.alongsSquared == 0f) { return 1f; } - // Fast path for horizontal gradients. - if (this.acrossX == 0f) - { - float denom = this.end.X - this.start.X; - return denom != 0f ? (x - this.start.X) / denom : 1f; - } - - // Fast path for vertical gradients. - if (this.acrossY == 0f) - { - float denom = this.end.Y - this.start.Y; - return denom != 0f ? (y - this.start.Y) / denom : 1f; - } - - // General case: project sample point onto the gradient axis. float deltaX = x - this.start.X; float deltaY = y - this.start.Y; - - // Compute perpendicular projection scalar. - float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared; - - // Determine projected point on the gradient line. - float projX = x - (k * this.alongY); - float projY = y + (k * this.alongX); - - // Compute distance from gradient start to projected point. - float dx = projX - this.start.X; - float dy = projY - this.start.Y; - - // Normalize to [0,1] range along the gradient length. - return this.length > 0f ? MathF.Sqrt((dx * dx) + (dy * dy)) / this.length : 1f; + return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared; } } } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index e115d863d..3c3372bbe 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -84,6 +84,11 @@ internal PatternBrush(PatternBrush brush) this.patternVector = brush.patternVector; } + /// + /// Gets the pattern color matrix. + /// + public DenseMatrix Pattern => this.pattern; + /// public override bool Equals(Brush? other) { diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 1fcae55f4..a5aac4fba 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -62,6 +62,31 @@ public RadialGradientBrush( this.radius1 = endRadius; } + /// + /// Gets the center of the starting circle. + /// + public PointF Center0 => this.center0; + + /// + /// Gets the radius of the starting circle. + /// + public float Radius0 => this.radius0; + + /// + /// Gets the center of the ending circle, or for single-circle form. + /// + public PointF? Center1 => this.center1; + + /// + /// Gets the radius of the ending circle, or for single-circle form. + /// + public float? Radius1 => this.radius1; + + /// + /// Gets a value indicating whether this is a two-circle radial gradient. + /// + public bool IsTwoCircle => this.center1.HasValue && this.radius1.HasValue; + /// public override bool Equals(Brush? other) { @@ -95,7 +120,7 @@ public override BrushApplicator CreateApplicator( this.radius0, this.center1, this.radius1, - this.ColorStops, + this.ColorStopsArray, this.RepetitionMode); /// diff --git a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs index 2ef27c2f8..c982fac55 100644 --- a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Helpers; + namespace SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Memory; @@ -34,10 +36,25 @@ public SweepGradientBrush( : base(repetitionMode, colorStops) { this.center = center; - this.startAngleDegrees = startAngleDegrees; - this.endAngleDegrees = endAngleDegrees; + this.startAngleDegrees = NormalizeDegrees(startAngleDegrees); + this.endAngleDegrees = NormalizeDegrees(endAngleDegrees); } + /// + /// Gets the center point of the sweep gradient. + /// + public PointF Center => this.center; + + /// + /// Gets the starting angle in degrees. + /// + public float StartAngleDegrees => this.startAngleDegrees; + + /// + /// Gets the ending angle in degrees. + /// + public float EndAngleDegrees => this.endAngleDegrees; + /// public override bool Equals(Brush? other) { @@ -60,6 +77,17 @@ public override int GetHashCode() this.startAngleDegrees, this.endAngleDegrees); + private static float NormalizeDegrees(float deg) + { + float d = deg % 360f; + if (d < 0f) + { + d += 360f; + } + + return d; + } + /// public override BrushApplicator CreateApplicator( Configuration configuration, @@ -70,10 +98,8 @@ public override BrushApplicator CreateApplicator( configuration, options, targetRegion, - this.center, - this.startAngleDegrees, - this.endAngleDegrees, - this.ColorStops, + this, + this.ColorStopsArray, this.RepetitionMode); /// @@ -101,41 +127,47 @@ private sealed class SweepGradientBrushApplicator : GradientBrushApplica /// The configuration instance to use when performing operations. /// The graphics options. /// The destination pixel region. - /// The center of the sweep gradient. - /// The start angle in degrees (clockwise). - /// The end angle in degrees (clockwise). + /// The sweep gradient brush. /// The gradient color stops (ratios in [0..1]). /// Defines how gradient colors are repeated outside [0..1]. public SweepGradientBrushApplicator( Configuration configuration, GraphicsOptions options, Buffer2DRegion targetRegion, - PointF center, - float startAngleDegrees, - float endAngleDegrees, + SweepGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) : base(configuration, options, targetRegion, colorStops, repetitionMode) { - this.cx = center.X; - this.cy = center.Y; + this.cx = brush.Center.X; + this.cy = brush.Center.Y; - float start = GeometryUtilities.DegreeToRadian(NormalizeDegrees(startAngleDegrees)); - float end = GeometryUtilities.DegreeToRadian(NormalizeDegrees(endAngleDegrees)); + float startRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees); + float endRad = GeometryUtilities.DegreeToRadian(brush.EndAngleDegrees); - float sweep = NormalizeDeltaRadians(end - start); + float sweep = NormalizeDeltaRadians(endRad - startRad); - // If sweep collapses numerically to ~0, treat as full circle. if (MathF.Abs(sweep) < 1e-6f) { sweep = Tau; } - this.startRad = start; + this.startRad = startRad; this.invSweep = 1f / sweep; this.isFullCircle = MathF.Abs(sweep - Tau) < 1e-6f; } + private static float NormalizeDeltaRadians(float delta) + { + float d = delta % Tau; + if (d <= 0f) + { + d += Tau; + } + + return d; + } + /// /// Calculates the position parameter along the sweep gradient for the given device-space point. /// The returned value is not clamped to [0..1]; repetition semantics are applied by the base class. @@ -185,27 +217,5 @@ protected override float PositionOnGradient(float x, float y) // Partial sweep: phase beyond sweep -> t > 1 (lets repetition mode handle clipping). return phase * this.invSweep; } - - private static float NormalizeDegrees(float deg) - { - float d = deg % 360f; - if (d < 0f) - { - d += 360f; - } - - return d; - } - - private static float NormalizeDeltaRadians(float delta) - { - float d = delta % Tau; - if (d <= 0f) - { - d += Tau; - } - - return d; - } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 347eb7c7a..eb1a93edf 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -120,7 +120,7 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, "FillPath_AliasedThreshold", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, "FillPath_AliasedThreshold", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1066,12 +1066,12 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider LineJoinValues { get; } = new() @@ -1129,7 +1129,7 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvi DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1179,7 +1179,7 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1225,7 +1225,7 @@ void DrawAction(DrawingCanvas canvas) DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, "FillPath_MultipleSeparate", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, "FillPath_MultipleSeparate", defaultImage, cpuRegionImage, nativeSurfaceImage); Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount >= 20); AssertCoverageExecutionAccounting(cpuRegionBackend); @@ -1292,7 +1292,7 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, "FillPath_EvenOdd", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, "FillPath_EvenOdd", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1336,7 +1336,7 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTripletNoRef(provider, "FillPath_LargeTileCount", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, "FillPath_LargeTileCount", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1445,7 +1445,7 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using (nativeSurfaceImage) { - DebugSaveBackendTripletNoRef(provider, "MultipleFlushes", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendTriplet(provider, "MultipleFlushes", defaultImage, cpuRegionImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1459,6 +1459,542 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag } } + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = new LinearGradientBrush( + new PointF(28, 28), + new PointF(228, 228), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.5F, Color.Green), + new ColorStop(1, Color.Blue)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_LinearGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new LinearGradientBrush( + new PointF(64, 64), + new PointF(128, 128), + GradientRepetitionMode.Repeat, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Purple)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_LinearGradient_Repeat", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RadialGradientBrush( + new PointF(128, 128), + 100F, + GradientRepetitionMode.None, + new ColorStop(0, Color.White), + new ColorStop(1, Color.DarkRed)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_RadialGradient_Single", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RadialGradientBrush( + new PointF(100, 100), + 20F, + new PointF(128, 128), + 110F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Navy)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_RadialGradient_TwoCircle", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new EllipticGradientBrush( + new PointF(128, 128), + new PointF(228, 128), + 0.6F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Cyan), + new ColorStop(1, Color.Magenta)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_EllipticGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = new SweepGradientBrush( + new PointF(128, 128), + 0F, + 360F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.33F, Color.Green), + new ColorStop(0.67F, Color.Blue), + new ColorStop(1, Color.Red)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_SweepGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new SweepGradientBrush( + new PointF(128, 128), + 45F, + 270F, + GradientRepetitionMode.Reflect, + new ColorStop(0, Color.Orange), + new ColorStop(1, Color.Teal)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_SweepGradient_PartialArc", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = Brushes.Horizontal(Color.Black, Color.White); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_PatternBrush_Horizontal", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = Brushes.ForwardDiagonal(Color.DarkGreen, Color.LightGray); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_PatternBrush_Diagonal", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "Red", PixelTypes.Rgba32)] + public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RecolorBrush(Color.Red, Color.Blue, 0.5F); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_RecolorBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new LinearGradientBrush( + new PointF(64, 128), + new PointF(192, 128), + new PointF(128, 64), + GradientRepetitionMode.None, + new ColorStop(0, Color.Coral), + new ColorStop(1, Color.SteelBlue)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_LinearGradient_ThreePoint", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + + [Theory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(8, 8, 240, 240); + Brush brush = new EllipticGradientBrush( + new PointF(128, 128), + new PointF(180, 160), + 0.4F, + GradientRepetitionMode.Reflect, + new ColorStop(0, Color.Gold), + new ColorStop(0.5F, Color.DarkViolet), + new ColorStop(1, Color.White)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "FillPath_EllipticGradient_Reflect", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + private static Buffer2DRegion GetFrameRegion(Image image) where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png index 88e1bfb77..ed8d3fa3d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc897cff98e55d62d410b225fe585bd4a6e8ef28d4b8bb08b71b498470cbcab7 -size 18965 +oid sha256:8efbb68954ec0d4e64a9e502282aeabea32c03310d53f9ceaa245153ff8f2642 +size 18940 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png new file mode 100644 index 000000000..dd7273ec4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb +size 4892 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png new file mode 100644 index 000000000..dd7273ec4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb +size 4892 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..dd7273ec4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb +size 4892 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png new file mode 100644 index 000000000..e2da3554e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16243d82d61d8ce66a9a8ce3584d9f22ec990aa79e31bd2aecc0de065efa2bae +size 99045 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png new file mode 100644 index 000000000..4a6e9867b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ecf0746020a0f89895a19f36135e48bbe7fdaf5069123da0b12942881d7cb77 +size 99042 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png new file mode 100644 index 000000000..4a6e9867b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ecf0746020a0f89895a19f36135e48bbe7fdaf5069123da0b12942881d7cb77 +size 99042 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png new file mode 100644 index 000000000..f3acaaca4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c52083eca151bfe6a69e2900d0bd19535e9e6d57dda489330d67c18274d313fc +size 5449 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png new file mode 100644 index 000000000..50d1ae9ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391ac2e3a2f949744cbafd394b7fac092ca24a82dbc80868496089a77348c4a2 +size 5455 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..50d1ae9ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391ac2e3a2f949744cbafd394b7fac092ca24a82dbc80868496089a77348c4a2 +size 5455 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png new file mode 100644 index 000000000..4c8c85716 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6831aad3db662c097dd6bc163eeeb80f90e802176dcbf7f29aa3696efae42d40 +size 1643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png new file mode 100644 index 000000000..0c25ee2b2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:645a37b9808db4273c7680a9543b664172a2a46700c72a4836489354db7a10c4 +size 3667 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0c25ee2b2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:645a37b9808db4273c7680a9543b664172a2a46700c72a4836489354db7a10c4 +size 3667 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png new file mode 100644 index 000000000..ab47e6ae3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f670831068c21e3151e5ff2f1a985bf9ec26445b74b815de5aba50316995d20a +size 1370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png new file mode 100644 index 000000000..fb153d398 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80d0f8a96b226d1a5034baa32ce5cbf14ef99a7a6ae7b75907c0271698a3a749 +size 1370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png new file mode 100644 index 000000000..fb153d398 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80d0f8a96b226d1a5034baa32ce5cbf14ef99a7a6ae7b75907c0271698a3a749 +size 1370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png new file mode 100644 index 000000000..30c3e9349 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a0ded646cdc61bfff585b39f1beaa3b444e25c9866474fff335fc1b828526ac +size 2209 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png new file mode 100644 index 000000000..30c3e9349 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a0ded646cdc61bfff585b39f1beaa3b444e25c9866474fff335fc1b828526ac +size 2209 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png new file mode 100644 index 000000000..30c3e9349 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a0ded646cdc61bfff585b39f1beaa3b444e25c9866474fff335fc1b828526ac +size 2209 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png new file mode 100644 index 000000000..82c778d4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb +size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png new file mode 100644 index 000000000..82c778d4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb +size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png new file mode 100644 index 000000000..82c778d4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb +size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png new file mode 100644 index 000000000..23b687e57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2824eed2bb429f76270556cbb939a7f32558fa5eb9d7ada891ab3c888f45b2 +size 10237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png new file mode 100644 index 000000000..23b687e57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2824eed2bb429f76270556cbb939a7f32558fa5eb9d7ada891ab3c888f45b2 +size 10237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png new file mode 100644 index 000000000..23b687e57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2824eed2bb429f76270556cbb939a7f32558fa5eb9d7ada891ab3c888f45b2 +size 10237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png new file mode 100644 index 000000000..5916a439a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:219f9ce0010b283867e39ca551b6c39a69a8cc8d35790634d554d064c6dcaaca +size 1549 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png new file mode 100644 index 000000000..5916a439a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:219f9ce0010b283867e39ca551b6c39a69a8cc8d35790634d554d064c6dcaaca +size 1549 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5916a439a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:219f9ce0010b283867e39ca551b6c39a69a8cc8d35790634d554d064c6dcaaca +size 1549 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png new file mode 100644 index 000000000..25a223a26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png new file mode 100644 index 000000000..25a223a26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png new file mode 100644 index 000000000..25a223a26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png new file mode 100644 index 000000000..ab8b52bcb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edf24575d55a7d0474ce2e5520db0873469ce94b0c42a06ff9c5647582d41327 +size 15203 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png new file mode 100644 index 000000000..58d7de17b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f184d9b980d6df10fc55706bf2125c95f2663969cdfd586041f6f987ac90127 +size 15195 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..58d7de17b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f184d9b980d6df10fc55706bf2125c95f2663969cdfd586041f6f987ac90127 +size 15195 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png new file mode 100644 index 000000000..377cc81e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d863787f2a3af2298ea34714f79c326e6444bf37c6961eed7d266d901c647b81 +size 15469 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png new file mode 100644 index 000000000..19121c6a7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dfcbf2edcbd4dbf44243bd18ff3246440e8b0739f05d0d4b3c2c8117a28d4f0 +size 15482 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png new file mode 100644 index 000000000..19121c6a7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dfcbf2edcbd4dbf44243bd18ff3246440e8b0739f05d0d4b3c2c8117a28d4f0 +size 15482 From ea688af70be6526cf82f154c478af80f93ba915c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Mar 2026 20:32:56 +1000 Subject: [PATCH 112/136] Replace DashPathSplitter with GenerateDashes extension --- samples/WebGPUWindowDemo/Program.cs | 3 +- .../WEBGPU_BACKEND_PROCESS.md | 4 +- .../WebGPUDrawingBackend.cs | 2 +- .../OutlinePathExtensions.cs | 141 ++---------------- .../PolygonGeometry/StrokedShapeGenerator.cs | 64 +------- .../Backends/DefaultDrawingBackend.cs | 2 +- .../Processing/Backends/PolygonScanning.MD | 4 +- .../Processing/StrokeOptions.cs | 16 +- ...PathSplitter.cs => SplitPathExtensions.cs} | 62 +++++++- ...StateIsolation_MatchesReference_Rgba32.png | 4 +- 10 files changed, 88 insertions(+), 214 deletions(-) rename src/ImageSharp.Drawing/{Processing/Backends/DashPathSplitter.cs => SplitPathExtensions.cs} (58%) diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index db75209c5..6be20ced5 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -28,7 +28,7 @@ public static unsafe class Program { private const int WindowWidth = 800; private const int WindowHeight = 600; - private const int BallCount = 10; + private const int BallCount = 50; // Silk.NET WebGPU API and windowing handles. private static WebGPU wgpu; @@ -38,7 +38,6 @@ public static unsafe class Program private static Instance* instance; private static Surface* surface; private static SurfaceConfiguration surfaceConfiguration; - private static SurfaceCapabilities surfaceCapabilities; private static Adapter* adapter; private static Device* device; private static Queue* queue; diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 026f6e380..0bdf34563 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -72,7 +72,7 @@ DrawingCanvasBatcher.Flush() For stroke definitions (`CompositionCoverageDefinition.IsStroke`), the backend performs stroke expansion on the GPU using `StrokeExpandComputeShader`: -1. **Dash splitting** (CPU): If the definition has a dash pattern, `DashPathSplitter.SplitDashes()` +1. **Dash splitting** (CPU): If the definition has a dash pattern, `SplitPathExtensions.GenerateDashes()` (shared with `DefaultDrawingBackend` in the core project) segments the centerline into open dash sub-paths before edge building. @@ -194,7 +194,7 @@ Edge preparation (path flattening, fixed-point conversion, CSR construction) run Both the CPU and GPU backends use per-band parallel stroke expansion - the CPU via `DefaultRasterizer.RasterizeStrokeRows` and the GPU via `StrokeExpandComputeShader`. Both share the same `StrokeEdgeFlags` enum and -`DashPathSplitter` (in the core project). The CPU backend fuses stroke expansion +`SplitPathExtensions.GenerateDashes` (in the core project). The CPU backend fuses stroke expansion directly into the rasterizer's band loop, while the GPU backend uses a separate compute dispatch that writes outline edges into pre-allocated per-band output slots sized by `ComputeOutlineEdgesPerCenterline()`. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index c497538ef..db76b1ad9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -506,7 +506,7 @@ private bool TryRenderPreparedFlush( { // For dashed strokes, split the path into dash segments on the CPU // so the GPU evaluates solid strokes on each dash segment. - strokePath = DashPathSplitter.SplitDashes(strokePath, definition.StrokeWidth, definition.StrokePattern.Span); + strokePath = strokePath.GenerateDashes(definition.StrokeWidth, definition.StrokePattern.Span); } float halfWidth = definition.StrokeWidth * 0.5f; diff --git a/src/ImageSharp.Drawing/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/OutlinePathExtensions.cs index cd4c08011..77f40996f 100644 --- a/src/ImageSharp.Drawing/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/OutlinePathExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Drawing.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Processing; @@ -98,144 +97,22 @@ public static IPath GenerateOutline( return path.GenerateOutline(width, strokeOptions); } - const float eps = 1e-6f; - const int maxPatternSegments = 10000; + IPath dashed = path.GenerateDashes(width, pattern, startOff); - // Compute the absolute pattern length in path units to detect degenerate patterns. - float patternLength = 0f; - for (int i = 0; i < pattern.Length; i++) - { - patternLength += MathF.Abs(pattern[i]) * width; - } - - // Fallback to a solid outline when the dash pattern is too small to be meaningful. - if (patternLength <= eps) + // GenerateDashes returns the original path when the pattern is degenerate + // or when segmentation would exceed safety limits; stroke it as solid. + if (ReferenceEquals(dashed, path)) { return path.GenerateOutline(width, strokeOptions); } - IEnumerable paths = path.Flatten(); - - List outlines = []; - List buffer = new(64); // arbitrary initial capacity hint. - - foreach (ISimplePath p in paths) + if (dashed == Path.Empty) { - bool online = !startOff; - int patternPos = 0; - float targetLength = pattern[patternPos] * width; - - ReadOnlySpan pts = p.Points.Span; - if (pts.Length < 2) - { - continue; - } - - // number of edges to traverse (no wrap for open paths) - int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; - float totalLength = 0f; - - // Compute total path length to estimate the number of dash segments to produce. - for (int j = 0; j < edgeCount; j++) - { - int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1; - totalLength += Vector2.Distance(pts[j], pts[nextIndex]); - } - - if (totalLength > eps) - { - // Avoid runaway segmentation by falling back when the dash count explodes. - float estimatedSegments = (totalLength / patternLength) * pattern.Length; - if (estimatedSegments > maxPatternSegments) - { - return path.GenerateOutline(width, strokeOptions); - } - } - - int i = 0; - Vector2 current = pts[0]; - - while (i < edgeCount) - { - int nextIndex = p.IsClosed ? (i + 1) % pts.Length : i + 1; - Vector2 next = pts[nextIndex]; - float segLen = Vector2.Distance(current, next); - - // Skip degenerate segments. - if (segLen <= eps) - { - current = next; - i++; - continue; - } - - // Accumulate into the current dash span when the segment is shorter than the target. - if (segLen + eps < targetLength) - { - buffer.Add(current); - current = next; - i++; - targetLength -= segLen; - continue; - } - - // Close out a dash span when the segment length matches the target length. - if (MathF.Abs(segLen - targetLength) <= eps) - { - buffer.Add(current); - buffer.Add(next); - - if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) - { - outlines.Add([.. buffer]); - } - - buffer.Clear(); - online = !online; - - current = next; - i++; - patternPos = (patternPos + 1) % pattern.Length; - targetLength = pattern[patternPos] * width; - continue; - } - - // Split inside this segment to end the current dash span. - float t = targetLength / segLen; // 0 < t < 1 here - Vector2 split = current + (t * (next - current)); - - buffer.Add(current); - buffer.Add(split); - - if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) - { - outlines.Add([.. buffer]); - } - - buffer.Clear(); - online = !online; - - current = split; // continue along the same geometric segment - - patternPos = (patternPos + 1) % pattern.Length; - targetLength = pattern[patternPos] * width; - } - - // flush tail of the last dash span, if any - if (buffer.Count > 0) - { - buffer.Add(current); // terminate at the true end position - - if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) - { - outlines.Add([.. buffer]); - } - - buffer.Clear(); - } + return Path.Empty; } - // Each outline span is stroked as an open polyline; the union cleans overlaps. - return StrokedShapeGenerator.GenerateStrokedShapes(outlines, width, strokeOptions); + // Each dash segment is an open sub-path; stroke expansion and boolean merge + // are handled by the generator. + return StrokedShapeGenerator.GenerateStrokedShapes(dashed, width, strokeOptions); } } diff --git a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs index d51d25532..d93f0fd6d 100644 --- a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -13,67 +13,6 @@ namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// internal static class StrokedShapeGenerator { - /// - /// Strokes a collection of dashed polyline spans and returns a merged outline. - /// - /// - /// The input spans. Each array is treated as an open polyline - /// and is stroked using the current stroker settings. - /// Spans that are null or contain fewer than 2 points are ignored. - /// - /// The stroke width in the caller's coordinate space. - /// The stroke geometry options. - /// - /// A representing the stroked outline after boolean merge. - /// - public static ComplexPolygon GenerateStrokedShapes(List spans, float width, StrokeOptions options) - { - // 1) Stroke each dashed span as open. - PCPolygon rings = new(spans.Count); - foreach (PointF[] span in spans) - { - if (span == null || span.Length < 2) - { - continue; - } - - Contour ring = new(span.Length); - for (int i = 0; i < span.Length; i++) - { - PointF p = span[i]; - ring.Add(new Vertex(p.X, p.Y)); - } - - rings.Add(ring); - } - - int count = rings.Count; - if (count == 0) - { - return new([]); - } - - PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options)); - - IPath[] shapes = new IPath[result.Count]; - int index = 0; - for (int i = 0; i < result.Count; i++) - { - Contour contour = result[i]; - PointF[] points = new PointF[contour.Count]; - - for (int j = 0; j < contour.Count; j++) - { - Vertex vertex = contour[j]; - points[j] = new PointF((float)vertex.X, (float)vertex.Y); - } - - shapes[index++] = new Polygon(points); - } - - return new(shapes); - } - /// /// Strokes a path and returns a merged outline from its flattened segments. /// @@ -168,7 +107,8 @@ private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions op LineCap.Round => PolygonClipper.LineCap.Round, LineCap.Square => PolygonClipper.LineCap.Square, _ => PolygonClipper.LineCap.Butt, - } + }, + NormalizeOutput = options.NormalizeOutput }; return o; diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 0c96dd5fd..857ec1670 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -162,7 +162,7 @@ private void FlushPreparedBatch( { // Dashed strokes: split into dash segments on the CPU, then stroke-expand // each segment via the per-band parallel path (same as solid strokes). - rasterPath = DashPathSplitter.SplitDashes(rasterPath, definition.StrokeWidth, definition.StrokePattern.Span); + rasterPath = rasterPath.GenerateDashes(definition.StrokeWidth, definition.StrokePattern.Span); // Recompute interest from the split path bounds with stroke expansion. float halfWidth = definition.StrokeWidth * 0.5f; diff --git a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD index 8d35bc744..16e6b5555 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD @@ -52,14 +52,14 @@ each parallel band only expands the centerline edges that overlap it. This avoid the cost of a serial full-path `GenerateOutline()` call and eliminates the intermediate `IPath` allocation for the expanded outline. -For dashed strokes, `DashPathSplitter` splits the centerline into dash segments +For dashed strokes, `SplitPathExtensions` splits the centerline into dash segments on the CPU before passing the result through the same per-band stroke expansion pipeline. ``` IPath (centerline) | - +--> [if dashed] DashPathSplitter.SplitDashes(path, strokeWidth, pattern) + +--> [if dashed] SplitPathExtensions.GenerateDashes(path, strokeWidth, pattern) | v path.Flatten() -> List (preserving open/closed state) diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs index 6430d9fbd..44e73969b 100644 --- a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -40,6 +40,16 @@ public sealed class StrokeOptions : IEquatable /// public InnerJoin InnerJoin { get; set; } = InnerJoin.Miter; + /// + /// Gets or sets a value indicating whether stroked contours should be normalized + /// by resolving self-intersections and overlaps before returning. + /// + /// + /// Defaults to false for maximum throughput. When disabled, callers should rasterize + /// with a non-zero winding fill rule. + /// + public bool NormalizeOutput { get; set; } + /// public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions); @@ -51,7 +61,8 @@ public bool Equals(StrokeOptions? other) this.ArcDetailScale == other.ArcDetailScale && this.LineJoin == other.LineJoin && this.LineCap == other.LineCap && - this.InnerJoin == other.InnerJoin; + this.InnerJoin == other.InnerJoin && + this.NormalizeOutput == other.NormalizeOutput; /// public override int GetHashCode() @@ -61,5 +72,6 @@ public override int GetHashCode() this.ArcDetailScale, this.LineJoin, this.LineCap, - this.InnerJoin); + this.InnerJoin, + this.NormalizeOutput); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs b/src/ImageSharp.Drawing/SplitPathExtensions.cs similarity index 58% rename from src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs rename to src/ImageSharp.Drawing/SplitPathExtensions.cs index 467d9aac0..b551121dc 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs +++ b/src/ImageSharp.Drawing/SplitPathExtensions.cs @@ -3,14 +3,30 @@ using System.Numerics; -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +namespace SixLabors.ImageSharp.Drawing; /// -/// Splits a path into dash segments without performing stroke expansion. -/// Each "on" dash segment is returned as an open sub-path. +/// Extensions to for splitting paths into dash segments +/// without performing stroke expansion. /// -public static class DashPathSplitter +public static class SplitPathExtensions { + // Safety limit: if the estimated number of dash segments exceeds this threshold, + // return the original path unsplit to avoid runaway segmentation from very short + // patterns applied to very long paths. + private const int MaxPatternSegments = 10000; + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern) + => path.GenerateDashes(strokeWidth, pattern, startOff: false); + /// /// Splits the given path into dash segments based on the provided pattern. /// Returns a composite path containing only the "on" segments as open sub-paths. @@ -18,8 +34,9 @@ public static class DashPathSplitter /// The centerline path to split. /// The stroke width (pattern elements are multiples of this). /// The dash pattern. Each element is a multiple of . + /// Whether the first item in the pattern is off rather than on. /// A path containing the "on" dash segments. - public static IPath SplitDashes(IPath path, float strokeWidth, ReadOnlySpan pattern) + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, bool startOff) { if (pattern.Length < 2) { @@ -28,12 +45,14 @@ public static IPath SplitDashes(IPath path, float strokeWidth, ReadOnlySpan eps) + { + float estimatedSegments = (totalLength / patternLength) * pattern.Length; + if (estimatedSegments > MaxPatternSegments) + { + return path; + } + } + int ei = 0; Vector2 current = pts[0]; @@ -65,6 +105,7 @@ public static IPath SplitDashes(IPath path, float strokeWidth, ReadOnlySpan 0) { if (online) @@ -134,7 +180,7 @@ public static IPath SplitDashes(IPath path, float strokeWidth, ReadOnlySpan Date: Sun, 8 Mar 2026 21:33:01 +1000 Subject: [PATCH 113/136] Add ReleaseFrameResources and WebGPU CPU cache key --- .../WebGPUDrawingBackend.cs | 10 ++++ .../WebGPUFlushContext.cs | 57 ++++++++++++++++--- .../Backends/DefaultDrawingBackend.cs | 9 +++ .../Processing/Backends/IDrawingBackend.cs | 11 ++++ .../Processing/DrawingCanvas{TPixel}.cs | 23 +++++++- .../Processing/DrawingCanvasBatcherTests.cs | 7 +++ .../Processing/DrawingCanvasTests.Process.cs | 7 +++ .../RasterizerDefaultsExtensionsTests.cs | 7 +++ 8 files changed, 120 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index db76b1ad9..2bd9aabb7 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -300,6 +300,16 @@ public void FlushCompositions( compositionBounds); } + /// + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TPixel : unmanaged, IPixel + { + nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(target); + WebGPUFlushContext.ReleaseCpuTargetEntries(targetIdentity); + } + /// /// Checks whether all scene commands are directly composable by WebGPU. /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 57d9c56ef..7ed81ccef 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -237,7 +237,8 @@ private WebGPUFlushContext( } context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, memoryAllocator, deviceState); - context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, initialUploadBounds); + nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(frame); + context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, targetIdentity, initialUploadBounds); return context; } catch @@ -308,6 +309,18 @@ public static void ClearFallbackStagingCache() FallbackStagingCache.Clear(); } + /// + /// Releases all cached CPU target resources associated with the specified target identity. + /// + /// The target frame identity whose cached resources should be released. + public static void ReleaseCpuTargetEntries(nint targetIdentity) + { + foreach (DeviceSharedState state in DeviceStateCache.Values) + { + state.ReleaseCpuTargetEntries(targetIdentity); + } + } + /// /// Clears all cached device-scoped shared state. /// @@ -621,6 +634,7 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability) private void InitializeCpuTarget( Buffer2DRegion cpuRegion, int pixelSizeInBytes, + nint targetIdentity, Rectangle? initialUploadBounds) where TPixel : unmanaged { @@ -630,7 +644,8 @@ private void InitializeCpuTarget( this.TextureFormat, width, height, - pixelSizeInBytes); + pixelSizeInBytes, + targetIdentity); Texture* targetTexture = lease.TargetTexture; TextureView* targetView = lease.TargetView; WgpuBuffer* readbackBuffer = lease.ReadbackBuffer; @@ -894,18 +909,36 @@ private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* /// The destination width. /// The destination height. /// The destination pixel size in bytes. + /// Identity of the target frame to prevent cache collisions. /// A lease for staging resources. public CpuTargetLease RentCpuTarget( TextureFormat textureFormat, int width, int height, - int pixelSizeInBytes) + int pixelSizeInBytes, + nint targetIdentity) { - CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes); + CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes, targetIdentity); CpuTargetEntry entry = this.cpuTargetCache.GetOrAdd(key, static _ => new CpuTargetEntry()); return entry.Rent(this.Api, this.Device, in key); } + /// + /// Releases and removes all CPU target cache entries matching the specified target identity. + /// + /// The target frame identity whose entries should be released. + public void ReleaseCpuTargetEntries(nint targetIdentity) + { + foreach (KeyValuePair pair in this.cpuTargetCache) + { + if (pair.Key.TargetIdentity == targetIdentity && + this.cpuTargetCache.TryRemove(pair.Key, out CpuTargetEntry? entry)) + { + entry.Dispose(this.Api); + } + } + } + /// /// Gets or creates a graphics pipeline used for composite rendering. /// @@ -1438,7 +1471,8 @@ internal readonly struct CpuTargetCacheKey( TextureFormat textureFormat, int width, int height, - int pixelSizeInBytes) : IEquatable + int pixelSizeInBytes, + nint targetIdentity) : IEquatable { /// /// Gets the texture format for the cached CPU target. @@ -1460,22 +1494,29 @@ internal readonly struct CpuTargetCacheKey( /// public int PixelSizeInBytes { get; } = pixelSizeInBytes; + /// + /// Gets the identity of the target frame to prevent different targets + /// with the same dimensions from sharing GPU texture content. + /// + public nint TargetIdentity { get; } = targetIdentity; + /// /// Determines whether this key equals another CPU target cache key. /// /// The key to compare. - /// if all dimensions and format match; otherwise . + /// if all fields match; otherwise . public bool Equals(CpuTargetCacheKey other) => this.TextureFormat == other.TextureFormat && this.Width == other.Width && this.Height == other.Height && - this.PixelSizeInBytes == other.PixelSizeInBytes; + this.PixelSizeInBytes == other.PixelSizeInBytes && + this.TargetIdentity == other.TargetIdentity; /// public override bool Equals(object? obj) => obj is CpuTargetCacheKey other && this.Equals(other); /// - public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes); + public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes, this.TargetIdentity); } /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 857ec1670..15b5eb31d 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -71,6 +71,15 @@ public void FlushCompositions( } } + /// + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TPixel : unmanaged, IPixel + { + // No cached resources to release for CPU-only backend. + } + /// public bool TryReadRegion( Configuration configuration, diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 29e5e5b58..e9cfe7a28 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -40,4 +40,15 @@ public bool TryReadRegion( Rectangle sourceRectangle, [NotNullWhen(true)] out Image? image) where TPixel : unmanaged, IPixel; + + /// + /// Releases any backend resources cached against the specified target frame. + /// + /// The pixel format. + /// Active processing configuration. + /// The target frame whose resources should be released. + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 5dd97706e..e5915be7a 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -47,6 +47,12 @@ public sealed class DrawingCanvas : IDrawingCanvas /// private readonly List> pendingImageResources = []; + /// + /// Indicates whether this canvas is the root owner of the target frame. + /// Only the root canvas releases backend resources on dispose. + /// + private readonly bool isRoot; + /// /// Tracks whether this instance has already been disposed. /// @@ -108,7 +114,8 @@ internal DrawingCanvas( backend, targetFrame, new DrawingCanvasBatcher(configuration, backend, targetFrame), - new DrawingCanvasState(options, clipPaths)) + new DrawingCanvasState(options, clipPaths), + isRoot: true) { } @@ -121,12 +128,14 @@ internal DrawingCanvas( /// The destination frame. /// The command batcher used for deferred composition. /// The default state used when no scoped state is active. + /// Whether this canvas is the root owner of the target frame. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, DrawingCanvasBatcher batcher, - DrawingCanvasState defaultState) + DrawingCanvasState defaultState, + bool isRoot) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); @@ -143,6 +152,7 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; + this.isRoot = isRoot; // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); @@ -277,7 +287,8 @@ public DrawingCanvas CreateRegion(Rectangle region) this.backend, childFrame, this.batcher, - this.ResolveState()); + this.ResolveState(), + isRoot: false); } /// @@ -1046,6 +1057,12 @@ public void Dispose() finally { this.DisposePendingImageResources(); + + if (this.isRoot) + { + this.backend.ReleaseFrameResources(this.configuration, this.targetFrame); + } + this.isDisposed = true; } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 75a88d3c9..bbc825bc7 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -173,5 +173,12 @@ public bool TryReadRegion( image = null; return false; } + + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TPixel : unmanaged, IPixel + { + } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index 57495f7c5..1f20530f5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -193,5 +193,12 @@ public bool TryReadRegion( image = cropped.CloneAs(); return true; } + + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TTargetPixel : unmanaged, IPixel + { + } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index d58e2cdfa..249babc7a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -65,5 +65,12 @@ public bool TryReadRegion( image = null; return false; } + + public void ReleaseFrameResources( + Configuration configuration, + ICanvasFrame target) + where TPixel : unmanaged, IPixel + { + } } } From 233b04ee9b375600288a75b6f98c8e7bb93bad05 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 9 Mar 2026 16:52:59 +1000 Subject: [PATCH 114/136] Add new text measuring tests --- .../ImageSharp.Drawing.csproj | 2 +- .../Processing/DrawingCanvas{TPixel}.cs | 25 ++- .../Processing/IDrawingCanvas.cs | 129 ++++++++--- .../DrawingCanvasTests.TextMeasuring.cs | 200 ++++++++++++++++++ ...enderedMetrics_MatchesReference_Rgba32.png | 3 + 5 files changed, 329 insertions(+), 30 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index a57ea8bc8..26b8cb03d 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -45,7 +45,7 @@ - + diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index e5915be7a..eaace40f3 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -610,7 +610,7 @@ public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) Guard.NotNull(text, nameof(text)); FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); - return RectangleF.FromLTRB(0, 0, advance.Width, advance.Height); + return RectangleF.FromLTRB(advance.Left, advance.Top, advance.Right, advance.Bottom); } /// @@ -624,6 +624,17 @@ public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text) return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); } + /// + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, string text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, textOptions); + return RectangleF.FromLTRB(renderableBounds.Left, renderableBounds.Top, renderableBounds.Right, renderableBounds.Bottom); + } + /// public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) { @@ -632,7 +643,7 @@ public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) Guard.NotNull(text, nameof(text)); FontRectangle size = TextMeasurer.MeasureSize(text, textOptions); - return RectangleF.FromLTRB(0, 0, size.Width, size.Height); + return RectangleF.FromLTRB(size.Left, size.Top, size.Right, size.Bottom); } /// @@ -655,6 +666,16 @@ public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds); } + /// + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + Guard.NotNull(text, nameof(text)); + + return TextMeasurer.TryMeasureCharacterRenderableBounds(text, textOptions, out bounds); + } + /// public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes) { diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 4f7b7d811..51c0e5a9a 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -236,70 +236,145 @@ public void DrawGlyphs( IReadOnlyList glyphs); /// - /// Measures the advance box of the specified text. + /// Measures the logical advance of the text in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// The measured advance as a rectangle in px units. + /// The logical advance rectangle of the text if it was to be rendered. + /// + /// This measurement reflects line-box height and horizontal or vertical text advance from the layout model. + /// It does not guarantee that all rendered glyph pixels fit within the returned rectangle. + /// Use for glyph ink bounds or + /// for the union of logical advance and rendered bounds. + /// public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text); /// - /// Measures the tight bounds of the specified text. + /// Measures the rendered glyph bounds of the text in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// The measured bounds rectangle in px units. + /// The rendered glyph bounds of the text if it was to be rendered. + /// + /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle + /// may be smaller or larger than the logical advance and may have a non-zero origin. + /// Use for the logical layout box or + /// for the union of both. + /// public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text); /// - /// Measures the size of the specified text. + /// Measures the full renderable bounds of the text in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// The measured size as a rectangle in px units. + /// + /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. + /// + /// + /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds. + /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. + /// + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, string text); + + /// + /// Measures the normalized rendered size of the text in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The rendered size of the text with the origin normalized to (0, 0). + /// + /// This is equivalent to measuring the rendered bounds and returning only the width and height. + /// Use when the returned X and Y offset are also required. + /// public RectangleF MeasureTextSize(RichTextOptions textOptions, string text); /// - /// Tries to measure per-character advances for the specified text. + /// Measures the logical advance of each laid-out character entry in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// Receives per-character advance metrics in px units. - /// if all character advances were measured; otherwise . + /// The list of per-entry logical advances of the text if it was to be rendered. + /// Whether any of the entries had non-empty advances. + /// + /// Each entry reflects the typographic advance width and height for one character. + /// Use for per-character ink bounds or + /// for the union of both. + /// public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances); /// - /// Tries to measure per-character bounds for the specified text. + /// Measures the rendered glyph bounds of each laid-out character entry in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// Receives per-character bounds in px units. - /// if all character bounds were measured; otherwise . + /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. + /// Whether any of the entries had non-empty bounds. + /// + /// Each entry reflects the tight ink bounds of one rendered glyph. + /// Use for per-character logical advances or + /// for the union of both. + /// public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds); /// - /// Tries to measure per-character sizes for the specified text. + /// Measures the full renderable bounds of each laid-out character entry in pixel units. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// Receives per-character sizes in px units. - /// if all character sizes were measured; otherwise . + /// The list of per-entry renderable bounds of the text if it was to be rendered. + /// Whether any of the entries had non-empty bounds. + /// + /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. + /// Use this when both typographic advance and rendered glyph overshoot must fit within the same rectangle. + /// + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds); + + /// + /// Measures the normalized rendered size of each laid-out character entry in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The list of per-entry rendered sizes with the origin normalized to (0, 0). + /// Whether any of the entries had non-empty dimensions. + /// + /// This is equivalent to measuring per-character bounds and returning only the width and height. + /// Use when the returned X and Y offset are also required. + /// public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes); /// - /// Counts the rendered text lines for the specified text. + /// Gets the number of laid-out lines contained within the text. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// The number of rendered lines. + /// The laid-out line count. public int CountTextLines(RichTextOptions textOptions, string text); /// - /// Gets line metrics for the specified text. + /// Gets per-line layout metrics for the supplied text. /// - /// Text layout options. + /// The text shaping and layout options. /// The text to measure. - /// An array of line metrics in px units. + /// + /// An array of in pixel units, one entry per laid-out line. + /// + /// + /// + /// The returned and are expressed + /// in the primary flow direction for the active layout mode. + /// + /// + /// , , and + /// are line-box positions relative to the current line origin and are suitable for drawing guide lines. + /// + /// + /// Horizontal layouts: Start = X position, Extent = width. + /// Vertical layouts: Start = Y position, Extent = height. + /// + /// public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text); /// diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs new file mode 100644 index 000000000..657fe3740 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs @@ -0,0 +1,200 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(600, 400, PixelTypes.Rgba32)] + public void TextMeasuring_RenderedMetrics_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 36); + const string text = "Sphinx of black quartz,\njudge my vow."; + + RichTextOptions textOptions = new(font) + { + Origin = new PointF(60, 60), + LineSpacing = 1.8F + }; + + canvas.Clear(Brushes.Solid(Color.White)); + + PointF origin = textOptions.Origin; + + // Line metrics: colored bands with ascender/baseline/descender guides. + int lineCount = canvas.CountTextLines(textOptions, text); + LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text); + Assert.Equal(lineCount, lineMetrics.Length); + + float lineOriginY = origin.Y; + Color[] bandColors = + [ + Color.LightCoral.WithAlpha(0.4F), + Color.Khaki.WithAlpha(0.6F), + Color.LightGreen.WithAlpha(0.4F), + ]; + + for (int i = 0; i < lineMetrics.Length; i++) + { + LineMetrics metrics = lineMetrics[i]; + float startX = origin.X + metrics.Start; + float endX = startX + metrics.Extent; + + canvas.Fill( + new RectangularPolygon(startX, lineOriginY, endX - startX, metrics.LineHeight), + Brushes.Solid(bandColors[i % bandColors.Length])); + + canvas.DrawLine( + Pens.Solid(Color.Teal.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Ascender), + new PointF(endX, lineOriginY + metrics.Ascender)); + + canvas.DrawLine( + Pens.Solid(Color.Crimson.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Baseline), + new PointF(endX, lineOriginY + metrics.Baseline)); + + canvas.DrawLine( + Pens.Solid(Color.DarkOrange.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Descender), + new PointF(endX, lineOriginY + metrics.Descender)); + + lineOriginY += metrics.LineHeight; + } + + // Character renderable bounds: outlined rectangles positioned at each glyph. + if (canvas.TryMeasureCharacterRenderableBounds(textOptions, text, out ReadOnlySpan charRenderableBounds)) + { + Color[] renderableColors = + [ + Color.Black, + Color.Black + ]; + + for (int i = 0; i < charRenderableBounds.Length; i++) + { + FontRectangle rb = charRenderableBounds[i].Bounds; + canvas.Draw( + Pens.Solid(renderableColors[i % renderableColors.Length], 1), + new RectangularPolygon(rb.X, rb.Y, rb.Width, rb.Height)); + } + } + + // Character bounds: alternating filled rectangles behind the glyphs. + if (canvas.TryMeasureCharacterBounds(textOptions, text, out ReadOnlySpan charBounds)) + { + Color[] charColors = + [ + Color.Gold.WithAlpha(0.5F), + Color.MediumPurple.WithAlpha(0.5F), + ]; + + for (int i = 0; i < charBounds.Length; i++) + { + FontRectangle b = charBounds[i].Bounds; + canvas.Fill( + new RectangularPolygon(b.X, b.Y, b.Width, b.Height), + Brushes.Solid(charColors[i % charColors.Length])); + } + } + + // Render the text. + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + + // Advance rectangle (green outline). + RectangleF advance = canvas.MeasureTextAdvance(textOptions, text); + canvas.Draw( + Pens.Solid(Color.SeaGreen, 2), + new RectangularPolygon(origin.X + advance.X, origin.Y + advance.Y, advance.Width, advance.Height)); + + // Bounds rectangle (dodger blue outline). + RectangleF bounds = canvas.MeasureTextBounds(textOptions, text); + canvas.Draw( + Pens.Solid(Color.DodgerBlue, 2), + new RectangularPolygon(bounds.X, bounds.Y, bounds.Width, bounds.Height)); + + // Renderable bounds rectangle (black outline). + RectangleF renderableBounds = canvas.MeasureTextRenderableBounds(textOptions, text); + canvas.Draw( + Pens.Solid(Color.Black, 2), + new RectangularPolygon(renderableBounds.X, renderableBounds.Y, renderableBounds.Width, renderableBounds.Height)); + + // Origin crosshair. + canvas.DrawLine(Pens.Solid(Color.Gray, 1), new PointF(origin.X - 12, origin.Y), new PointF(origin.X + 12, origin.Y)); + canvas.DrawLine(Pens.Solid(Color.Gray, 1), new PointF(origin.X, origin.Y - 12), new PointF(origin.X, origin.Y + 12)); + + // Key. + Font keyFont = TestFontUtilities.GetFont(TestFonts.OpenSans, 13); + float keyX = 16; + float keyY = 280; + const float swatchW = 24; + const float swatchH = 12; + const float rowHeight = 20; + const float labelOffset = swatchW + 6; + + (string Label, Color Color1, Color? Color2, bool IsFill)[] keyEntries = + [ + ("Advance", Color.SeaGreen, null, false), + ("Bounds", Color.DodgerBlue, null, false), + ("Renderable Bounds", Color.Black, null, false), + ("Ascender", Color.Teal.WithAlpha(0.9F), null, true), + ("Baseline", Color.Crimson.WithAlpha(0.9F), null, true), + ("Descender", Color.DarkOrange.WithAlpha(0.9F), null, true), + ("Char Bounds", Color.Gold.WithAlpha(0.5F), Color.MediumPurple.WithAlpha(0.5F), true), + ("Char Renderable Bounds", Color.Black, null, false), + ("Line Band", Color.LightCoral.WithAlpha(0.4F), Color.Khaki.WithAlpha(0.6F), true), + ("Origin", Color.Gray, null, false), + ]; + + for (int i = 0; i < keyEntries.Length; i++) + { + float col = i < 5 ? 0 : 300; + float row = i < 5 ? i : i - 5; + float x = keyX + col; + float y = keyY + (row * rowHeight); + float halfW = swatchW / 2F; + + if (keyEntries[i].IsFill) + { + if (keyEntries[i].Color2 is Color c2) + { + canvas.Fill( + new RectangularPolygon(x, y, halfW, swatchH), + Brushes.Solid(keyEntries[i].Color1)); + canvas.Fill( + new RectangularPolygon(x + halfW, y, halfW, swatchH), + Brushes.Solid(c2)); + } + else + { + canvas.Fill( + new RectangularPolygon(x, y, swatchW, swatchH), + Brushes.Solid(keyEntries[i].Color1)); + } + } + else + { + canvas.Draw( + Pens.Solid(keyEntries[i].Color1, 2), + new RectangularPolygon(x, y, swatchW, swatchH)); + } + + RichTextOptions keyTextOptions = new(keyFont) { Origin = new PointF(x + labelOffset, y - 1) }; + canvas.DrawText(keyTextOptions, keyEntries[i].Label, Brushes.Solid(Color.Black), pen: null); + } + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png new file mode 100644 index 000000000..92636e503 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5c55275454be86d1a502d0f5d4c601f6833447a6585d27255a200fbeff503d +size 33142 From 837df162c593aafa84611066cb0db41f6666bd30 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:00:13 +1000 Subject: [PATCH 115/136] Migrate drawing transforms to Matrix4x4 --- samples/DrawShapesWithImageSharp/Program.cs | 2 +- samples/WebGPUWindowDemo/Program.cs | 4 +- .../Shaders/CompositeComputeShader.cs | 2 +- src/ImageSharp.Drawing/ArcLineSegment.cs | 4 +- src/ImageSharp.Drawing/ComplexPolygon.cs | 2 +- .../CubicBezierLineSegment.cs | 4 +- src/ImageSharp.Drawing/EllipsePolygon.cs | 2 +- src/ImageSharp.Drawing/EmptyPath.cs | 2 +- .../Helpers/MatrixUtilities.cs | 28 ++ src/ImageSharp.Drawing/ILineSegment.cs | 2 +- src/ImageSharp.Drawing/IPath.cs | 2 +- src/ImageSharp.Drawing/IPathCollection.cs | 2 +- .../ImageSharp.Drawing.csproj | 2 +- src/ImageSharp.Drawing/InternalPath.cs | 4 +- src/ImageSharp.Drawing/LinearLineSegment.cs | 4 +- src/ImageSharp.Drawing/Path.cs | 2 +- src/ImageSharp.Drawing/PathBuilder.cs | 24 +- src/ImageSharp.Drawing/PathCollection.cs | 2 +- src/ImageSharp.Drawing/PathExtensions.cs | 16 +- src/ImageSharp.Drawing/Polygon.cs | 2 +- src/ImageSharp.Drawing/Processing/Brush.cs | 8 + .../Processing/DrawingCanvas{TPixel}.cs | 161 ++++++---- .../Processing/DrawingOptions.cs | 8 +- .../DrawingOptionsDefaultsExtensions.cs | 16 +- .../Processing/EllipticGradientBrush.cs | 31 +- .../Processing/GradientBrush.cs | 5 +- .../Processing/IDrawingCanvas.cs | 42 +-- .../Processing/LinearGradientBrush.cs | 9 + .../Processing/RadialGradientBrush.cs | 16 + .../RichTextGlyphRenderer.Brushes.cs | 28 +- .../Processing/RichTextGlyphRenderer.cs | 10 +- .../Processing/SweepGradientBrush.cs | 18 ++ src/ImageSharp.Drawing/RectangularPolygon.cs | 2 +- src/ImageSharp.Drawing/RegularPolygon.cs | 2 +- src/ImageSharp.Drawing/Star.cs | 2 +- .../Text/BaseGlyphBuilder.cs | 2 +- src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs | 2 +- .../Text/GlyphPathCollection.cs | 2 +- .../Text/PathGlyphBuilder.cs | 2 +- .../Drawing/DrawPolygon.cs | 6 +- .../Drawing/EllipseStressTest.cs | 2 +- .../Drawing/FillPolygon.cs | 12 +- .../Drawing/FillRectangle.cs | 6 +- .../Issues/Issue_244.cs | 2 +- .../Backends/WebGPUDrawingBackendTests.cs | 293 ++++++++++++++++-- .../Processing/DrawingCanvasBatcherTests.cs | 6 +- .../DrawingCanvasTests.BrushAndPenStyles.cs | 6 +- .../Processing/DrawingCanvasTests.Clear.cs | 16 +- .../DrawingCanvasTests.DrawImage.cs | 6 +- .../DrawingCanvasTests.PathBuilderDraw.cs | 2 +- .../DrawingCanvasTests.PathBuilderFill.cs | 4 +- .../DrawingCanvasTests.PathRules.cs | 6 +- .../DrawingCanvasTests.RegionAndState.cs | 30 +- .../DrawingCanvasTests.StrokeOptions.cs | 2 +- .../Processing/DrawingCanvasTests.Text.cs | 10 +- .../DrawingCanvasTests.TextMeasuring.cs | 20 +- ...thDrawingCanvasTests.AntialiasThreshold.cs | 10 +- .../ProcessWithDrawingCanvasTests.Blending.cs | 20 +- .../ProcessWithDrawingCanvasTests.Clear.cs | 4 +- .../ProcessWithDrawingCanvasTests.Clip.cs | 2 +- ...ithDrawingCanvasTests.FillOutsideBounds.cs | 4 +- .../ProcessWithDrawingCanvasTests.FillPath.cs | 14 +- ...ssWithDrawingCanvasTests.FillSolidBrush.cs | 6 +- ...cessWithDrawingCanvasTests.ImageBrushes.cs | 16 +- .../ProcessWithDrawingCanvasTests.Polygons.cs | 46 +-- ...rocessWithDrawingCanvasTests.Primitives.cs | 12 +- .../ProcessWithDrawingCanvasTests.Recolor.cs | 2 +- ...rocessWithDrawingCanvasTests.Robustness.cs | 36 +-- .../ProcessWithDrawingCanvasTests.Text.cs | 12 +- .../Rasterization/DefaultRasterizerTests.cs | 2 +- .../Shapes/PathBuilderTests.cs | 6 +- .../Shapes/PathExtentionTests.cs | 32 +- .../Shapes/RectangleTests.cs | 4 +- .../Shapes/TestShapes.cs | 6 +- .../TestUtilities/DebugDraw.cs | 4 +- .../TestUtilities/PolygonFactory.cs | 6 +- ...arallelEllipsesWithDifferentRatio_0.10.png | 4 +- ...arallelEllipsesWithDifferentRatio_0.40.png | 4 +- ...arallelEllipsesWithDifferentRatio_0.80.png | 4 +- ...arallelEllipsesWithDifferentRatio_1.00.png | 4 +- ...arallelEllipsesWithDifferentRatio_1.20.png | 4 +- ...arallelEllipsesWithDifferentRatio_1.60.png | 4 +- ...arallelEllipsesWithDifferentRatio_2.00.png | 4 +- ...lipsesWithDifferentRatio_0.10_AT_00deg.png | 4 +- ...lipsesWithDifferentRatio_0.10_AT_30deg.png | 4 +- ...lipsesWithDifferentRatio_0.10_AT_45deg.png | 4 +- ...lipsesWithDifferentRatio_0.10_AT_90deg.png | 4 +- ...lipsesWithDifferentRatio_0.40_AT_00deg.png | 4 +- ...lipsesWithDifferentRatio_0.40_AT_30deg.png | 4 +- ...lipsesWithDifferentRatio_0.40_AT_45deg.png | 4 +- ...lipsesWithDifferentRatio_0.40_AT_90deg.png | 4 +- ...lipsesWithDifferentRatio_0.80_AT_00deg.png | 4 +- ...lipsesWithDifferentRatio_0.80_AT_30deg.png | 4 +- ...lipsesWithDifferentRatio_0.80_AT_45deg.png | 4 +- ...lipsesWithDifferentRatio_0.80_AT_90deg.png | 4 +- ...lipsesWithDifferentRatio_1.00_AT_00deg.png | 4 +- ...lipsesWithDifferentRatio_1.00_AT_30deg.png | 4 +- ...lipsesWithDifferentRatio_1.00_AT_45deg.png | 4 +- ...lipsesWithDifferentRatio_1.00_AT_90deg.png | 4 +- ...rm_StarWarsCrawl_StarWarsCrawl_Default.png | 3 + ...rsCrawl_StarWarsCrawl_WebGPU_CPURegion.png | 3 + ...awl_StarWarsCrawl_WebGPU_NativeSurface.png | 3 + ...tput_FillPath_EllipticGradient_Default.png | 4 +- ...Path_EllipticGradient_WebGPU_CPURegion.png | 4 +- ..._EllipticGradient_WebGPU_NativeSurface.png | 4 +- ...lPath_EllipticGradient_Reflect_Default.png | 4 +- ...ipticGradient_Reflect_WebGPU_CPURegion.png | 4 +- ...cGradient_Reflect_WebGPU_NativeSurface.png | 4 +- 108 files changed, 845 insertions(+), 431 deletions(-) create mode 100644 src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index 4cf0ecade..c602d3cf3 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -267,7 +267,7 @@ public static void SaveImageWithPath(this IPathCollection collection, IPath shap { // Fill the canvas background and draw our shape. canvas.Fill(Brushes.Solid(Color.DarkBlue)); - canvas.Fill(shape, Brushes.Solid(Color.White.WithAlpha(.25F))); + canvas.Fill(Brushes.Solid(Color.White.WithAlpha(.25F)), shape); // Draw our path collection. canvas.Fill(Brushes.Solid(Color.HotPink), collection); diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index 6be20ced5..0f8653fed 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -304,7 +304,7 @@ private static void OnRender(double deltaTime) { ref Ball ball = ref balls[i]; EllipsePolygon ellipse = new(ball.X, ball.Y, ball.Radius); - canvas.Fill(ellipse, Brushes.Solid(ball.Color)); + canvas.Fill(Brushes.Solid(ball.Color), ellipse); } // Flush submits all queued draw operations to the GPU compositor and @@ -367,7 +367,7 @@ private static void DrawScrollingText(DrawingCanvas canvas, int w, int h continue; } - canvas.Fill(path.Transform(translation), textBrush); + canvas.Fill(textBrush, path.Transform(new Matrix4x4(translation))); } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 3d310cac3..40eeff8db 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -275,7 +275,7 @@ fn elliptic_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { if rx_sq < 1e-20 { return 0.0; } if ry_sq < 1e-20 { return 0.0; } - return rotated_x * rotated_x / rx_sq + rotated_y * rotated_y / ry_sq; + return sqrt(rotated_x * rotated_x / rx_sq + rotated_y * rotated_y / ry_sq); } // Sweep (angular) gradient. Computes radians and sweep from raw degrees. diff --git a/src/ImageSharp.Drawing/ArcLineSegment.cs b/src/ImageSharp.Drawing/ArcLineSegment.cs index b897d1d9b..eed5dafa5 100644 --- a/src/ImageSharp.Drawing/ArcLineSegment.cs +++ b/src/ImageSharp.Drawing/ArcLineSegment.cs @@ -93,7 +93,7 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn /// /// The transformation matrix. /// An with the matrix applied to it. - public ILineSegment Transform(Matrix3x2 matrix) + public ILineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -110,7 +110,7 @@ public ILineSegment Transform(Matrix3x2 matrix) } /// - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); private static PointF[] EllipticArcFromEndParams( PointF from, diff --git a/src/ImageSharp.Drawing/ComplexPolygon.cs b/src/ImageSharp.Drawing/ComplexPolygon.cs index dcdda4067..2e5b75313 100644 --- a/src/ImageSharp.Drawing/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/ComplexPolygon.cs @@ -67,7 +67,7 @@ public ComplexPolygon(params IPath[] paths) public RectangleF Bounds => this.bounds ??= this.CalcBounds(); /// - public IPath Transform(Matrix3x2 matrix) + public IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs index d655caacc..fbfe86662 100644 --- a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -81,7 +81,7 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control /// /// The matrix. /// A line segment with the matrix applied to it. - public CubicBezierLineSegment Transform(Matrix3x2 matrix) + public CubicBezierLineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -100,7 +100,7 @@ public CubicBezierLineSegment Transform(Matrix3x2 matrix) } /// - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); private static PointF[] GetDrawingPoints(PointF[] controlPoints) { diff --git a/src/ImageSharp.Drawing/EllipsePolygon.cs b/src/ImageSharp.Drawing/EllipsePolygon.cs index 6b9cac56f..4456c4bf5 100644 --- a/src/ImageSharp.Drawing/EllipsePolygon.cs +++ b/src/ImageSharp.Drawing/EllipsePolygon.cs @@ -59,7 +59,7 @@ public EllipsePolygon(float x, float y, float radius) } /// - public override IPath Transform(Matrix3x2 matrix) + public override IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/EmptyPath.cs b/src/ImageSharp.Drawing/EmptyPath.cs index 6789db399..ef39f221c 100644 --- a/src/ImageSharp.Drawing/EmptyPath.cs +++ b/src/ImageSharp.Drawing/EmptyPath.cs @@ -35,5 +35,5 @@ public sealed class EmptyPath : IPath public IEnumerable Flatten() => []; /// - public IPath Transform(Matrix3x2 matrix) => this; + public IPath Transform(Matrix4x4 matrix) => this; } diff --git a/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs new file mode 100644 index 000000000..6d0168044 --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Provides helper methods for extracting properties from transformation matrices. +/// +internal static class MatrixUtilities +{ + /// + /// Extracts the average 2D scale factor from a . + /// This is the mean of the X and Y axis scale magnitudes, suitable for + /// uniformly scaling radii under non-uniform or projective transforms. + /// + /// The transformation matrix. + /// The average scale factor. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetAverageScale(in Matrix4x4 matrix) + { + float sx = MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)); + float sy = MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)); + return (sx + sy) * 0.5f; + } +} diff --git a/src/ImageSharp.Drawing/ILineSegment.cs b/src/ImageSharp.Drawing/ILineSegment.cs index 19d485e7d..9cd9dee7f 100644 --- a/src/ImageSharp.Drawing/ILineSegment.cs +++ b/src/ImageSharp.Drawing/ILineSegment.cs @@ -29,5 +29,5 @@ public interface ILineSegment /// /// The matrix. /// A line segment with the matrix applied to it. - ILineSegment Transform(Matrix3x2 matrix); + ILineSegment Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/IPath.cs b/src/ImageSharp.Drawing/IPath.cs index 4e8be5840..bd305e38e 100644 --- a/src/ImageSharp.Drawing/IPath.cs +++ b/src/ImageSharp.Drawing/IPath.cs @@ -31,7 +31,7 @@ public interface IPath /// /// The matrix. /// A new path with the matrix applied to it. - public IPath Transform(Matrix3x2 matrix); + public IPath Transform(Matrix4x4 matrix); /// /// Returns this path with all figures closed. diff --git a/src/ImageSharp.Drawing/IPathCollection.cs b/src/ImageSharp.Drawing/IPathCollection.cs index ef2834721..5d2780235 100644 --- a/src/ImageSharp.Drawing/IPathCollection.cs +++ b/src/ImageSharp.Drawing/IPathCollection.cs @@ -20,5 +20,5 @@ public interface IPathCollection : IEnumerable /// /// The matrix. /// A new path collection with the matrix applied to it. - public IPathCollection Transform(Matrix3x2 matrix); + public IPathCollection Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 26b8cb03d..f91d2a3f1 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -46,7 +46,7 @@ - + diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs index 58fc69610..ec142b91f 100644 --- a/src/ImageSharp.Drawing/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -169,11 +169,11 @@ internal SegmentInfo PointAlongPath(float distanceAlongPath) Vector2 delta = a - b; float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); - Matrix3x2 transform = Matrix3x2.CreateRotation(angle - MathF.PI) * Matrix3x2.CreateTranslation(b.X, b.Y); + Matrix4x4 transform = Matrix4x4.CreateRotationZ(angle - MathF.PI) * Matrix4x4.CreateTranslation(b.X, b.Y, 0); return new SegmentInfo { - Point = Vector2.Transform(new Vector2(distanceAlongPath, 0), transform), + Point = PointF.Transform(new PointF(distanceAlongPath, 0), transform), Angle = angle }; } diff --git a/src/ImageSharp.Drawing/LinearLineSegment.cs b/src/ImageSharp.Drawing/LinearLineSegment.cs index 303482ec3..f410b7063 100644 --- a/src/ImageSharp.Drawing/LinearLineSegment.cs +++ b/src/ImageSharp.Drawing/LinearLineSegment.cs @@ -72,7 +72,7 @@ public LinearLineSegment(PointF[] points) /// /// A line segment with the matrix applied to it. /// - public LinearLineSegment Transform(Matrix3x2 matrix) + public LinearLineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -95,5 +95,5 @@ public LinearLineSegment Transform(Matrix3x2 matrix) /// /// The matrix. /// A line segment with the matrix applied to it. - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); } diff --git a/src/ImageSharp.Drawing/Path.cs b/src/ImageSharp.Drawing/Path.cs index 4ddf0c421..803483226 100644 --- a/src/ImageSharp.Drawing/Path.cs +++ b/src/ImageSharp.Drawing/Path.cs @@ -93,7 +93,7 @@ public Path(params ILineSegment[] segments) this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints); /// - public virtual IPath Transform(Matrix3x2 matrix) + public virtual IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs index ffc29cce6..f18b0d92b 100644 --- a/src/ImageSharp.Drawing/PathBuilder.cs +++ b/src/ImageSharp.Drawing/PathBuilder.cs @@ -12,17 +12,17 @@ namespace SixLabors.ImageSharp.Drawing; public class PathBuilder { private readonly List
figures = []; - private readonly Matrix3x2 defaultTransform; + private readonly Matrix4x4 defaultTransform; private Figure currentFigure; - private Matrix3x2 currentTransform; - private Matrix3x2 setTransform; + private Matrix4x4 currentTransform; + private Matrix4x4 setTransform; private Vector2 currentPoint; /// /// Initializes a new instance of the class. /// public PathBuilder() - : this(Matrix3x2.Identity) + : this(Matrix4x4.Identity) { } @@ -30,7 +30,7 @@ public PathBuilder() /// Initializes a new instance of the class. ///
/// The default transform. - public PathBuilder(Matrix3x2 defaultTransform) + public PathBuilder(Matrix4x4 defaultTransform) { this.defaultTransform = defaultTransform; this.Clear(); @@ -41,19 +41,19 @@ public PathBuilder(Matrix3x2 defaultTransform) /// Gets the current transformation matrix. ///
/// - /// Returns a copy of the matrix. Because is a value type, + /// Returns a copy of the matrix. Because is a value type, /// modifications to the returned value do not affect the internal state. To change the transform, - /// call . + /// call . /// /// The current transformation matrix. - public Matrix3x2 Transform => this.currentTransform; + public Matrix4x4 Transform => this.currentTransform; /// /// Sets the translation to be applied to all items to follow being applied to the . /// /// The transform. /// The . - public PathBuilder SetTransform(Matrix3x2 transform) + public PathBuilder SetTransform(Matrix4x4 transform) { this.setTransform = transform; this.currentTransform = this.setTransform * this.defaultTransform; @@ -68,7 +68,7 @@ public PathBuilder SetTransform(Matrix3x2 transform) public PathBuilder SetOrigin(PointF origin) { // The new origin should be transformed based on the default transform - this.setTransform.Translation = origin; + this.setTransform.Translation = new Vector3(origin.X, origin.Y, 0); this.currentTransform = this.setTransform * this.defaultTransform; return this; @@ -80,7 +80,7 @@ public PathBuilder SetOrigin(PointF origin) /// The . public PathBuilder ResetTransform() { - this.setTransform = Matrix3x2.Identity; + this.setTransform = Matrix4x4.Identity; this.currentTransform = this.setTransform * this.defaultTransform; return this; @@ -92,7 +92,7 @@ public PathBuilder ResetTransform() /// The . public PathBuilder ResetOrigin() { - this.setTransform.Translation = Vector2.Zero; + this.setTransform.Translation = Vector3.Zero; this.currentTransform = this.setTransform * this.defaultTransform; return this; diff --git a/src/ImageSharp.Drawing/PathCollection.cs b/src/ImageSharp.Drawing/PathCollection.cs index 9ae4bc739..2da4c7f81 100644 --- a/src/ImageSharp.Drawing/PathCollection.cs +++ b/src/ImageSharp.Drawing/PathCollection.cs @@ -63,7 +63,7 @@ private RectangleF CalcBounds() public IEnumerator GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); /// - public IPathCollection Transform(Matrix3x2 matrix) + public IPathCollection Transform(Matrix4x4 matrix) { IPath[] result = new IPath[this.paths.Length]; diff --git a/src/ImageSharp.Drawing/PathExtensions.cs b/src/ImageSharp.Drawing/PathExtensions.cs index a2cffd2c5..a0dcd38ca 100644 --- a/src/ImageSharp.Drawing/PathExtensions.cs +++ b/src/ImageSharp.Drawing/PathExtensions.cs @@ -17,7 +17,7 @@ public static partial class PathExtensions /// The radians to rotate the path. /// A with a rotate transform applied. public static IPathCollection Rotate(this IPathCollection path, float radians) - => path.Transform(Matrix3x2Extensions.CreateRotation(radians, RectangleF.Center(path.Bounds))); + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); /// /// Creates a path rotated by the specified degrees around its center. @@ -35,7 +35,7 @@ public static IPathCollection RotateDegree(this IPathCollection shape, float deg /// The translation position. /// A with a translate transform applied. public static IPathCollection Translate(this IPathCollection path, PointF position) - => path.Transform(Matrix3x2.CreateTranslation(position)); + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// /// Creates a path translated by the supplied position @@ -55,7 +55,7 @@ public static IPathCollection Translate(this IPathCollection path, float x, floa /// The amount to scale along the Y axis. /// A with a translate transform applied. public static IPathCollection Scale(this IPathCollection path, float scaleX, float scaleY) - => path.Transform(Matrix3x2.CreateScale(scaleX, scaleY, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path translated by the supplied position @@ -64,7 +64,7 @@ public static IPathCollection Scale(this IPathCollection path, float scaleX, flo /// The amount to scale along both the x and y axis. /// A with a translate transform applied. public static IPathCollection Scale(this IPathCollection path, float scale) - => path.Transform(Matrix3x2.CreateScale(scale, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path rotated by the specified radians around its center. @@ -73,7 +73,7 @@ public static IPathCollection Scale(this IPathCollection path, float scale) /// The radians to rotate the path. /// A with a rotate transform applied. public static IPath Rotate(this IPath path, float radians) - => path.Transform(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds))); + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); /// /// Creates a path rotated by the specified degrees around its center. @@ -91,7 +91,7 @@ public static IPath RotateDegree(this IPath shape, float degree) /// The translation position. /// A with a translate transform applied. public static IPath Translate(this IPath path, PointF position) - => path.Transform(Matrix3x2.CreateTranslation(position)); + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// /// Creates a path translated by the supplied position @@ -111,7 +111,7 @@ public static IPath Translate(this IPath path, float x, float y) /// The amount to scale along the Y axis. /// A with a translate transform applied. public static IPath Scale(this IPath path, float scaleX, float scaleY) - => path.Transform(Matrix3x2.CreateScale(scaleX, scaleY, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path translated by the supplied position @@ -120,7 +120,7 @@ public static IPath Scale(this IPath path, float scaleX, float scaleY) /// The amount to scale along both the x and y axis. /// A with a translate transform applied. public static IPath Scale(this IPath path, float scale) - => path.Transform(Matrix3x2.CreateScale(scale, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Calculates the approximate length of the path as though each segment were unrolled into a line. diff --git a/src/ImageSharp.Drawing/Polygon.cs b/src/ImageSharp.Drawing/Polygon.cs index e928d32e6..469a836ae 100644 --- a/src/ImageSharp.Drawing/Polygon.cs +++ b/src/ImageSharp.Drawing/Polygon.cs @@ -78,7 +78,7 @@ internal Polygon(ILineSegment[] segments, bool owned) public override bool IsClosed => true; /// - public override IPath Transform(Matrix3x2 matrix) + public override IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/Processing/Brush.cs b/src/ImageSharp.Drawing/Processing/Brush.cs index 6ae966e48..73776b558 100644 --- a/src/ImageSharp.Drawing/Processing/Brush.cs +++ b/src/ImageSharp.Drawing/Processing/Brush.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -36,6 +37,13 @@ public abstract BrushApplicator CreateApplicator( RectangleF region) where TPixel : unmanaged, IPixel; + /// + /// Returns a new brush with its defining geometry transformed by the given matrix. + /// + /// The transformation matrix to apply. + /// A transformed brush, or this if the brush has no spatial parameters. + public virtual Brush Transform(Matrix4x4 matrix) => this; + /// public abstract bool Equals(Brush? other); diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index eaace40f3..0f7d22689 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -304,28 +304,28 @@ public void Clear(Brush brush) } /// - public void Clear(Rectangle region, Brush brush) + public void Clear(Brush brush, Rectangle region) { DrawingCanvasState state = this.ResolveState(); DrawingOptions options = state.Options.CloneForClearOperation(); - this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(region, brush)); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, region)); } /// - public void Clear(IPath path, Brush brush) + public void Clear(Brush brush, IPath path) { DrawingCanvasState state = this.ResolveState(); DrawingOptions options = state.Options.CloneForClearOperation(); - this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(path, brush)); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, path)); } /// public void Fill(Brush brush) - => this.Fill(this.Bounds, brush); + => this.Fill(brush, this.Bounds); /// - public void Fill(Rectangle region, Brush brush) - => this.Fill(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), brush); + public void Fill(Brush brush, Rectangle region) + => this.Fill(brush, new RectangularPolygon(region.X, region.Y, region.Width, region.Height)); /// public void Fill(Brush brush, IPathCollection paths) @@ -333,19 +333,19 @@ public void Fill(Brush brush, IPathCollection paths) Guard.NotNull(paths, nameof(paths)); foreach (IPath path in paths) { - this.Fill(path, brush); + this.Fill(brush, path); } } /// - public void Fill(PathBuilder pathBuilder, Brush brush) + public void Fill(Brush brush, PathBuilder pathBuilder) { Guard.NotNull(pathBuilder, nameof(pathBuilder)); - this.Fill(pathBuilder.Build(), brush); + this.Fill(brush, pathBuilder.Build()); } /// - public void Fill(IPath path, Brush brush) + public void Fill(Brush brush, IPath path) { this.EnsureNotDisposed(); Guard.NotNull(path, nameof(path)); @@ -356,13 +356,17 @@ public void Fill(IPath path, Brush brush) IPath closed = path.AsClosedPath(); - IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity - ? closed - : closed.Transform(effectiveOptions.Transform); + Brush effectiveBrush = brush; + IPath effectivePath = closed; + if (effectiveOptions.Transform != Matrix4x4.Identity) + { + effectivePath = closed.Transform(effectiveOptions.Transform); + effectiveBrush = brush.Transform(effectiveOptions.Transform); + } - transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); + effectivePath = ApplyClipPaths(effectivePath, effectiveOptions.ShapeOptions, state.ClipPaths); - this.PrepareCompositionCore(transformedPath, brush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); + this.PrepareCompositionCore(effectivePath, effectiveBrush, effectiveOptions, RasterizerSamplingOrigin.PixelBoundary); } /// @@ -391,7 +395,7 @@ public void Process(IPath path, Action operation) DrawingOptions effectiveOptions = state.Options; IPath closed = path.AsClosedPath(); - IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity + IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity ? closed : closed.Transform(effectiveOptions.Transform); transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); @@ -474,7 +478,7 @@ public void Draw(Pen pen, IPath path) DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; - IPath transformedPath = effectiveOptions.Transform == Matrix3x2.Identity + IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity ? path : path.Transform(effectiveOptions.Transform); @@ -508,13 +512,17 @@ public void Draw(Pen pen, IPath path) /// public void DrawText( RichTextOptions textOptions, - string text, + ReadOnlySpan text, Brush? brush, Pen? pen) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return; + } DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; @@ -603,105 +611,149 @@ public void DrawGlyphs( } /// - public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text) + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); return RectangleF.FromLTRB(advance.Left, advance.Top, advance.Right, advance.Bottom); } /// - public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text) + public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } FontRectangle bounds = TextMeasurer.MeasureBounds(text, textOptions); return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); } /// - public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, string text) + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, textOptions); return RectangleF.FromLTRB(renderableBounds.Left, renderableBounds.Top, renderableBounds.Right, renderableBounds.Bottom); } /// - public RectangleF MeasureTextSize(RichTextOptions textOptions, string text) + public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } FontRectangle size = TextMeasurer.MeasureSize(text, textOptions); return RectangleF.FromLTRB(size.Left, size.Top, size.Right, size.Bottom); } /// - public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances) + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan advances) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + advances = []; + return false; + } return TextMeasurer.TryMeasureCharacterAdvances(text, textOptions, out advances); } /// - public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds) + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + bounds = []; + return false; + } return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds); } /// - public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds) + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + bounds = []; + return false; + } return TextMeasurer.TryMeasureCharacterRenderableBounds(text, textOptions, out bounds); } /// - public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes) + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan sizes) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + sizes = []; + return false; + } return TextMeasurer.TryMeasureCharacterSizes(text, textOptions, out sizes); } /// - public int CountTextLines(RichTextOptions textOptions, string text) + public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return 0; + } return TextMeasurer.CountLines(text, textOptions); } /// - public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text) + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan text) { this.EnsureNotDisposed(); Guard.NotNull(textOptions, nameof(textOptions)); - Guard.NotNull(text, nameof(text)); + + if (text.IsEmpty) + { + return []; + } return TextMeasurer.GetLineMetrics(text, textOptions); } @@ -798,7 +850,7 @@ private void DrawImageCore( } // Phase 2: Apply canvas transform to image content when requested. - if (effectiveOptions.Transform != Matrix3x2.Identity) + if (effectiveOptions.Transform != Matrix4x4.Identity) { Image transformed = CreateTransformedDrawImage( brushImage, @@ -843,7 +895,7 @@ private void DrawImageCore( renderDestinationRect.Width, renderDestinationRect.Height); - this.Fill(destinationPath, brush); + this.Fill(brush, destinationPath); } finally { @@ -1278,21 +1330,22 @@ private static Image CreateScaledDrawImage( private static Image CreateTransformedDrawImage( Image image, RectangleF destinationRect, - Matrix3x2 transform, + Matrix4x4 transform, IResampler? sampler, out RectangleF transformedDestinationRect) { // Source space: pixel coordinates in the untransformed source image (0..Width, 0..Height). // Destination space: where that image would land on the canvas without any extra transform. // This matrix maps source -> destination by scaling to destination size then translating to destination origin. - Matrix3x2 sourceToDestination = Matrix3x2.CreateScale( + Matrix4x4 sourceToDestination = Matrix4x4.CreateScale( destinationRect.Width / image.Width, - destinationRect.Height / image.Height) - * Matrix3x2.CreateTranslation(destinationRect.X, destinationRect.Y); + destinationRect.Height / image.Height, + 1) + * Matrix4x4.CreateTranslation(destinationRect.X, destinationRect.Y, 0); // Apply the canvas transform after source->destination placement: // source -> destination -> transformed-canvas. - Matrix3x2 sourceToTransformedCanvas = sourceToDestination * transform; + Matrix4x4 sourceToTransformedCanvas = sourceToDestination * transform; // Compute the transformed axis-aligned bounds so we know how large the output bitmap must be. transformedDestinationRect = TransformRectangle( @@ -1306,8 +1359,8 @@ private static Image CreateTransformedDrawImage( // ImageSharp.Transform expects output coordinates relative to the output bitmap origin (0,0). // Shift transformed-canvas coordinates so transformedDestinationRect.Left/Top becomes 0,0. - Matrix3x2 sourceToTarget = sourceToTransformedCanvas - * Matrix3x2.CreateTranslation(-transformedDestinationRect.X, -transformedDestinationRect.Y); + Matrix4x4 sourceToTarget = sourceToTransformedCanvas + * Matrix4x4.CreateTranslation(-transformedDestinationRect.X, -transformedDestinationRect.Y, 0); // Resample source pixels into the target bitmap using the computed source->target mapping. return image.Clone(ctx => ctx.Transform( @@ -1346,12 +1399,12 @@ private static RectangleF MapSourceClipToDestination( /// Input rectangle. /// Transform matrix. /// Axis-aligned bounds of the transformed rectangle. - private static RectangleF TransformRectangle(RectangleF rectangle, Matrix3x2 matrix) + private static RectangleF TransformRectangle(RectangleF rectangle, Matrix4x4 matrix) { - Vector2 topLeft = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); - Vector2 topRight = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); - Vector2 bottomLeft = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); - Vector2 bottomRight = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + PointF topLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Top), matrix); + PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix); + PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix); + PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs index fe29b76e6..8924f629c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -21,13 +21,13 @@ public DrawingOptions() { this.graphicsOptions = new GraphicsOptions(); this.shapeOptions = new ShapeOptions(); - this.Transform = Matrix3x2.Identity; + this.Transform = Matrix4x4.Identity; } internal DrawingOptions( GraphicsOptions graphicsOptions, ShapeOptions shapeOptions, - Matrix3x2 transform) + Matrix4x4 transform) { DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions)); DebugGuard.NotNull(shapeOptions, nameof(shapeOptions)); @@ -67,7 +67,7 @@ public ShapeOptions ShapeOptions /// /// Gets or sets the affine transform matrix applied to vector geometry before rasterization. /// Can be used to translate, rotate, scale, or skew shapes. - /// Defaults to . + /// Defaults to . /// - public Matrix3x2 Transform { get; set; } + public Matrix4x4 Transform { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs index cd06ee8c4..942ae2318 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public static class DrawingOptionsDefaultsExtensions { - private const string DrawingTransformMatrixKey = "DrawingTransformMatrix3x2"; + private const string DrawingTransformMatrixKey = "DrawingTransformMatrix4x4"; /// /// Gets the default shape processing options against The source image processing context. @@ -26,7 +26,7 @@ public static DrawingOptions GetDrawingOptions(this IImageProcessingContext cont /// The image processing context to store default against. /// The matrix to use. /// The passed in to allow chaining. - public static IImageProcessingContext SetDrawingTransform(this IImageProcessingContext context, Matrix3x2 matrix) + public static IImageProcessingContext SetDrawingTransform(this IImageProcessingContext context, Matrix4x4 matrix) { context.Properties[DrawingTransformMatrixKey] = matrix; return context; @@ -37,7 +37,7 @@ public static IImageProcessingContext SetDrawingTransform(this IImageProcessingC /// /// The configuration to store default against. /// The default matrix to use. - public static void SetDrawingTransform(this Configuration configuration, Matrix3x2 matrix) + public static void SetDrawingTransform(this Configuration configuration, Matrix4x4 matrix) => configuration.Properties[DrawingTransformMatrixKey] = matrix; /// @@ -45,9 +45,9 @@ public static void SetDrawingTransform(this Configuration configuration, Matrix3 /// /// The image processing context to retrieve defaults from. /// The matrix. - public static Matrix3x2 GetDrawingTransform(this IImageProcessingContext context) + public static Matrix4x4 GetDrawingTransform(this IImageProcessingContext context) { - if (context.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix3x2 go) + if (context.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix4x4 go) { return go; } @@ -62,14 +62,14 @@ public static Matrix3x2 GetDrawingTransform(this IImageProcessingContext context /// /// The configuration to retrieve defaults from. /// The globally configured default matrix. - public static Matrix3x2 GetDrawingTransform(this Configuration configuration) + public static Matrix4x4 GetDrawingTransform(this Configuration configuration) { - if (configuration.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix3x2 go) + if (configuration.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix4x4 go) { return go; } - return Matrix3x2.Identity; + return Matrix4x4.Identity; } /// diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index bed8ac50e..73f571a5f 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -51,6 +52,34 @@ public EllipticGradientBrush( /// public float AxisRatio { get; } + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.Center, matrix); + PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix); + + // Compute a point on the perpendicular (secondary) axis and transform it. + float refDx = this.ReferenceAxisEnd.X - this.Center.X; + float refDy = this.ReferenceAxisEnd.Y - this.Center.Y; + float refLen = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondLen = refLen * this.AxisRatio; + + // Perpendicular direction (rotated 90 degrees). + PointF secondEnd = new( + this.Center.X + (-refDy / refLen * secondLen), + this.Center.Y + (refDx / refLen * secondLen)); + PointF tSec = PointF.Transform(secondEnd, matrix); + + // Derive new ratio from transformed lengths. + float newRefLen = MathF.Sqrt( + ((tRef.X - tc.X) * (tRef.X - tc.X)) + ((tRef.Y - tc.Y) * (tRef.Y - tc.Y))); + float newSecLen = MathF.Sqrt( + ((tSec.X - tc.X) * (tSec.X - tc.X)) + ((tSec.Y - tc.Y) * (tSec.Y - tc.Y))); + float newRatio = newRefLen > 0f ? newSecLen / newRefLen : this.AxisRatio; + + return new EllipticGradientBrush(tc, tRef, newRatio, this.RepetitionMode, this.ColorStopsArray); + } + /// public override BrushApplicator CreateApplicator( Configuration configuration, @@ -123,7 +152,7 @@ protected override float PositionOnGradient(float x, float y) float xSquared = xR * xR; float ySquared = yR * yR; - return (xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared); + return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared)); } } } diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 05c221204..fddda4b2a 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -124,7 +124,10 @@ protected GradientBrushApplicator( { get { - float positionOnCompleteGradient = this.PositionOnGradient(x + 0.5f, y + 0.5f); + float fx = x + 0.5f; + float fy = y + 0.5f; + + float positionOnCompleteGradient = this.PositionOnGradient(fx, fy); switch (this.repetitionMode) { diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 51c0e5a9a..31157cd20 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -76,16 +76,16 @@ public interface IDrawingCanvas : IDisposable /// /// Clears a local region using the given brush and clear-style composition options. /// - /// Region to clear in local coordinates. /// Brush used to shade destination pixels during clear. - public void Clear(Rectangle region, Brush brush); + /// Region to clear in local coordinates. + public void Clear(Brush brush, Rectangle region); /// /// Clears a path region using the given brush and clear-style composition options. /// - /// The path region to clear. /// Brush used to shade destination pixels during clear. - public void Clear(IPath path, Brush brush); + /// The path region to clear. + public void Clear(Brush brush, IPath path); /// /// Fills the whole canvas using the given brush. @@ -96,9 +96,9 @@ public interface IDrawingCanvas : IDisposable /// /// Fills a local region using the given brush. /// - /// Region to fill in local coordinates. /// Brush used to shade destination pixels. - public void Fill(Rectangle region, Brush brush); + /// Region to fill in local coordinates. + public void Fill(Brush brush, Rectangle region); /// /// Fills all paths in a collection using the given brush and drawing options. @@ -110,16 +110,16 @@ public interface IDrawingCanvas : IDisposable /// /// Fills a path built by the provided builder using the given brush. /// - /// The path builder describing the fill region. /// Brush used to shade covered pixels. - public void Fill(PathBuilder pathBuilder, Brush brush); + /// The path builder describing the fill region. + public void Fill(Brush brush, PathBuilder pathBuilder); /// /// Fills a path in local coordinates using the given brush. /// - /// The path to fill. /// Brush used to shade covered pixels. - public void Fill(IPath path, Brush brush); + /// The path to fill. + public void Fill(Brush brush, IPath path); /// /// Applies an image-processing operation to a local region. @@ -215,7 +215,7 @@ public interface IDrawingCanvas : IDisposable /// Optional pen used to outline glyphs. public void DrawText( RichTextOptions textOptions, - string text, + ReadOnlySpan text, Brush? brush, Pen? pen); @@ -247,7 +247,7 @@ public void DrawGlyphs( /// Use for glyph ink bounds or /// for the union of logical advance and rendered bounds. /// - public RectangleF MeasureTextAdvance(RichTextOptions textOptions, string text); + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan text); /// /// Measures the rendered glyph bounds of the text in pixel units. @@ -261,7 +261,7 @@ public void DrawGlyphs( /// Use for the logical layout box or /// for the union of both. /// - public RectangleF MeasureTextBounds(RichTextOptions textOptions, string text); + public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan text); /// /// Measures the full renderable bounds of the text in pixel units. @@ -276,7 +276,7 @@ public void DrawGlyphs( /// rectangle and the rendered glyph bounds. /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. /// - public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, string text); + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text); /// /// Measures the normalized rendered size of the text in pixel units. @@ -288,7 +288,7 @@ public void DrawGlyphs( /// This is equivalent to measuring the rendered bounds and returning only the width and height. /// Use when the returned X and Y offset are also required. /// - public RectangleF MeasureTextSize(RichTextOptions textOptions, string text); + public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan text); /// /// Measures the logical advance of each laid-out character entry in pixel units. @@ -302,7 +302,7 @@ public void DrawGlyphs( /// Use for per-character ink bounds or /// for the union of both. /// - public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, string text, out ReadOnlySpan advances); + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan advances); /// /// Measures the rendered glyph bounds of each laid-out character entry in pixel units. @@ -316,7 +316,7 @@ public void DrawGlyphs( /// Use for per-character logical advances or /// for the union of both. /// - public bool TryMeasureCharacterBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds); + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds); /// /// Measures the full renderable bounds of each laid-out character entry in pixel units. @@ -330,7 +330,7 @@ public void DrawGlyphs( /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. /// Use this when both typographic advance and rendered glyph overshoot must fit within the same rectangle. /// - public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, string text, out ReadOnlySpan bounds); + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds); /// /// Measures the normalized rendered size of each laid-out character entry in pixel units. @@ -343,7 +343,7 @@ public void DrawGlyphs( /// This is equivalent to measuring per-character bounds and returning only the width and height. /// Use when the returned X and Y offset are also required. /// - public bool TryMeasureCharacterSizes(RichTextOptions textOptions, string text, out ReadOnlySpan sizes); + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan sizes); /// /// Gets the number of laid-out lines contained within the text. @@ -351,7 +351,7 @@ public void DrawGlyphs( /// The text shaping and layout options. /// The text to measure. /// The laid-out line count. - public int CountTextLines(RichTextOptions textOptions, string text); + public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan text); /// /// Gets per-line layout metrics for the supplied text. @@ -375,7 +375,7 @@ public void DrawGlyphs( /// Vertical layouts: Start = Y position, Extent = height. /// /// - public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, string text); + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan text); /// /// Draws an image source region into a destination rectangle. diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index e8a785be1..6452ec915 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -64,6 +65,14 @@ public LinearGradientBrush( /// public PointF EndPoint { get; } + /// + public override Brush Transform(Matrix4x4 matrix) + => new LinearGradientBrush( + PointF.Transform(this.StartPoint, matrix), + PointF.Transform(this.EndPoint, matrix), + this.RepetitionMode, + this.ColorStopsArray); + /// public override bool Equals(Brush? other) { diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index a5aac4fba..c339134d1 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; /// @@ -87,6 +89,20 @@ public RadialGradientBrush( /// public bool IsTwoCircle => this.center1.HasValue && this.radius1.HasValue; + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc0 = PointF.Transform(this.center0, matrix); + float scale = MatrixUtilities.GetAverageScale(in matrix); + if (this.IsTwoCircle) + { + PointF tc1 = PointF.Transform(this.center1!.Value, matrix); + return new RadialGradientBrush(tc0, this.radius0 * scale, tc1, this.radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray); + } + + return new RadialGradientBrush(tc0, this.radius0 * scale, this.RepetitionMode, this.ColorStopsArray); + } + /// public override bool Equals(Brush? other) { diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs index f9e044838..c35c40856 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs @@ -5,6 +5,7 @@ using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -20,7 +21,7 @@ internal sealed partial class RichTextGlyphRenderer /// A transform to apply to the brush coordinates. /// The resulting brush, or if the paint is unsupported. /// if a brush could be created; otherwise, . - public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix3x2 transform, [NotNullWhen(true)] out Brush? brush) + public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 transform, [NotNullWhen(true)] out Brush? brush) { brush = null; @@ -53,7 +54,7 @@ public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix3x2 tr /// The transform to apply to the gradient points. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -68,12 +69,12 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr // Apply any transform defined on the paint. if (!transform.IsIdentity) { - p0 = Vector2.Transform(p0, transform); - p1 = Vector2.Transform(p1, transform); + p0 = PointF.Transform(p0, transform); + p1 = PointF.Transform(p1, transform); if (p2.HasValue) { - p2 = Vector2.Transform(p2.Value, transform); + p2 = PointF.Transform(p2.Value, transform); } } @@ -94,7 +95,7 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr /// The transform to apply to the gradient center point. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -105,13 +106,18 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr // Apply any transform defined on the paint. PointF center0 = paint.Center0; PointF center1 = paint.Center1; + float radius0 = paint.Radius0; + float radius1 = paint.Radius1; if (!transform.IsIdentity) { - center0 = Vector2.Transform(center0, transform); - center1 = Vector2.Transform(center1, transform); + center0 = PointF.Transform(center0, transform); + center1 = PointF.Transform(center1, transform); + float scale = MatrixUtilities.GetAverageScale(in transform); + radius0 *= scale; + radius1 *= scale; } - brush = new RadialGradientBrush(center0, paint.Radius0, center1, paint.Radius1, mode, stops); + brush = new RadialGradientBrush(center0, radius0, center1, radius1, mode, stops); return true; } @@ -122,7 +128,7 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr /// The transform to apply to the gradient center point. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -134,7 +140,7 @@ private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix PointF center = paint.Center; if (!transform.IsIdentity) { - center = Vector2.Transform(center, transform); + center = PointF.Transform(center, transform); } brush = new SweepGradientBrush(center, paint.StartAngle, paint.EndAngle, mode, stops); diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs index d7e0a187e..b917811b4 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs @@ -454,7 +454,7 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star } // Scale about the chosen anchor so the fixed edge stays in place. - outline = outline.Transform(Matrix3x2.CreateScale(scale, anchor)); + outline = outline.Transform(Matrix4x4.CreateScale(scale.X, scale.Y, 1, new Vector3(anchor, 0))); } } @@ -752,14 +752,14 @@ private void TransformGlyph(in FontRectangle bounds) /// /// Computes the combined translation + rotation matrix that places a glyph - /// along the text path. For linear text (no path), returns . + /// along the text path. For linear text (no path), returns . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Matrix3x2 ComputeTransform(in FontRectangle bounds) + private Matrix4x4 ComputeTransform(in FontRectangle bounds) { if (this.path is null) { - return Matrix3x2.Identity; + return Matrix4x4.Identity; } // Find the point of this intersection along the given path. @@ -769,7 +769,7 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds) // Now offset to our target point since we're aligning the top-left location of our glyph against the path. Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - return Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); + return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); } /// diff --git a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs index c982fac55..b29b36c05 100644 --- a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -55,6 +56,23 @@ public SweepGradientBrush( /// public float EndAngleDegrees => this.endAngleDegrees; + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.center, matrix); + + float startRad = GeometryUtilities.DegreeToRadian(this.startAngleDegrees); + float endRad = GeometryUtilities.DegreeToRadian(this.endAngleDegrees); + + PointF startDir = PointF.Transform(new PointF(this.center.X + MathF.Cos(startRad), this.center.Y + MathF.Sin(startRad)), matrix); + PointF endDir = PointF.Transform(new PointF(this.center.X + MathF.Cos(endRad), this.center.Y + MathF.Sin(endRad)), matrix); + + float newStart = MathF.Atan2(startDir.Y - tc.Y, startDir.X - tc.X) * (180f / MathF.PI); + float newEnd = MathF.Atan2(endDir.Y - tc.Y, endDir.X - tc.X) * (180f / MathF.PI); + + return new SweepGradientBrush(tc, newStart, newEnd, this.RepetitionMode, this.ColorStopsArray); + } + /// public override bool Equals(Brush? other) { diff --git a/src/ImageSharp.Drawing/RectangularPolygon.cs b/src/ImageSharp.Drawing/RectangularPolygon.cs index aec7e31e5..f23514602 100644 --- a/src/ImageSharp.Drawing/RectangularPolygon.cs +++ b/src/ImageSharp.Drawing/RectangularPolygon.cs @@ -156,7 +156,7 @@ public static explicit operator RectangularPolygon(Polygon polygon) => new(polygon.Bounds.X, polygon.Bounds.Y, polygon.Bounds.Width, polygon.Bounds.Height); /// - public IPath Transform(Matrix3x2 matrix) + public IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/RegularPolygon.cs b/src/ImageSharp.Drawing/RegularPolygon.cs index b914d5389..44d92d3db 100644 --- a/src/ImageSharp.Drawing/RegularPolygon.cs +++ b/src/ImageSharp.Drawing/RegularPolygon.cs @@ -70,7 +70,7 @@ private static LinearLineSegment CreateSegment(PointF location, float radius, in PointF[] points = new PointF[vertices]; for (int i = 0; i < vertices; i++) { - PointF rotated = PointF.Transform(distanceVector, Matrix3x2.CreateRotation(current)); + PointF rotated = PointF.Transform(distanceVector, Matrix4x4.CreateRotationZ(current)); points[i] = rotated + location; diff --git a/src/ImageSharp.Drawing/Star.cs b/src/ImageSharp.Drawing/Star.cs index ba5a261ca..241b1bd44 100644 --- a/src/ImageSharp.Drawing/Star.cs +++ b/src/ImageSharp.Drawing/Star.cs @@ -87,7 +87,7 @@ private static LinearLineSegment CreateSegment(Vector2 location, float innerRadi distance = distanceVectorInner; } - Vector2 rotated = Vector2.Transform(distance, Matrix3x2.CreateRotation(current)); + Vector2 rotated = PointF.Transform(distance, Matrix4x4.CreateRotationZ(current)); points[i] = rotated + location; diff --git a/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs index 76213345a..0870a5744 100644 --- a/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs @@ -68,7 +68,7 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// with the specified transform applied to all incoming glyph geometry. /// /// A matrix transform applied to every point received from the font engine. - public BaseGlyphBuilder(Matrix3x2 transform) => this.Builder = new PathBuilder(transform); + public BaseGlyphBuilder(Matrix4x4 transform) => this.Builder = new PathBuilder(transform); /// /// Gets the flattened paths captured for all glyphs/graphemes. diff --git a/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs index da4a0d4f5..0011f932b 100644 --- a/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs +++ b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs @@ -100,7 +100,7 @@ private GlyphLayerInfo( /// public GlyphLayerKind Kind { get; } - internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix3x2 matrix) + internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix4x4 matrix) => new( info.StartIndex, info.Count, diff --git a/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs index 46d08a9f9..b628e75ea 100644 --- a/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs +++ b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs @@ -68,7 +68,7 @@ internal GlyphPathCollection(List paths, List layers) /// /// A new with the matrix applied to it. /// - public GlyphPathCollection Transform(Matrix3x2 matrix) + public GlyphPathCollection Transform(Matrix4x4 matrix) { List transformed = new(this.paths.Count); diff --git a/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs index 2c843e2ed..1f6144adb 100644 --- a/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs @@ -64,7 +64,7 @@ private void TransformGlyph(in FontRectangle bounds) // Translate so the glyph's top-left aligns with the path point, // then rotate around the path point to follow the tangent. Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); this.Builder.SetTransform(matrix); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index b7f3448b0..f205e6548 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -52,7 +52,7 @@ public abstract class DrawPolygon protected abstract float Thickness { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) => - [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60)))]; + [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix4x4.CreateScale(60, 60, 1)))]; [GlobalSetup] public void Setup() @@ -229,8 +229,8 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs index 015a9f0e8..7aed5cd8d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs @@ -36,7 +36,7 @@ public void DrawImageSharp() float y = this.Rand(this.height); EllipsePolygon ellipse = new(new PointF(x, y), r); - canvas.Fill(ellipse, Brushes.Solid(brushColor)); + canvas.Fill(Brushes.Solid(brushColor), ellipse); canvas.Draw(Pens.Solid(penColor, this.Rand(5)), ellipse); } })); diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs index c7e79c67c..25b22948c 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs @@ -36,7 +36,7 @@ public abstract class FillPolygon protected abstract int Height { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) - => [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60)))]; + => [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix4x4.CreateScale(60, 60, 1)))]; [GlobalSetup] public void Setup() @@ -104,7 +104,7 @@ public void ImageSharp() { foreach (Polygon polygon in this.polygons) { - canvas.Fill(polygon, Processing.Brushes.Solid(Color.White)); + canvas.Fill(Processing.Brushes.Solid(Color.White), polygon); } })); @@ -142,8 +142,8 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } @@ -172,8 +172,8 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Utah"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-60, -40) - * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-60, -40, 0) + * Matrix4x4.CreateScale(60, 60, 1); return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs index 3af511560..d0be4f328 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs @@ -34,7 +34,7 @@ public Size FillRectangleCore() using (Image image = new(800, 800)) { image.Mutate(x => x.ProcessWithCanvas( - canvas => canvas.Fill(new Rectangle(10, 10, 190, 140), Processing.Brushes.Solid(Color.HotPink)))); + canvas => canvas.Fill(Processing.Brushes.Solid(Color.HotPink), new Rectangle(10, 10, 190, 140)))); return new Size(image.Width, image.Height); } @@ -47,14 +47,14 @@ public Size FillPolygonCore() { image.Mutate(x => x.ProcessWithCanvas( canvas => canvas.Fill( + Processing.Brushes.Solid(Color.HotPink), new Polygon( [ new PointF(10, 10), new PointF(200, 10), new PointF(200, 150), new PointF(10, 150) - ]), - Processing.Brushes.Solid(Color.HotPink)))); + ])))); return new Size(image.Width, image.Height); } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs index 280173539..1375e254b 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs @@ -11,7 +11,7 @@ public class Issue_244 public void DoesNotHang() { PathBuilder pathBuilder = new(); - Matrix3x2 transform = Matrix3x2.CreateRotation(-0.04433158f, new Vector2(948, 640)); + Matrix4x4 transform = new(Matrix3x2.CreateRotation(-0.04433158f, new Vector2(948, 640))); pathBuilder.SetTransform(transform); pathBuilder.AddQuadraticBezier(new PointF(-2147483648, 677), new PointF(-2147483648, 675), new PointF(-2147483648, 675)); IPath path = pathBuilder.Build(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index eb1a93edf..234e7e7c8 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -6,6 +6,7 @@ // WebGPU adapter request failed with status 'Unavailable' // It's also failing in Windows CI with "Test host process crashed : Fatal error.0xC0000005" // TODO: Ask the Silk.NET team for help. +using System.Numerics; using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -47,7 +48,7 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(polygon, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -101,7 +102,7 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage EllipsePolygon ellipse = new(256, 256, 200, 150); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -146,7 +147,7 @@ public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvid void DrawAction(DrawingCanvas canvas) { canvas.Clear(clearBrush); - canvas.Fill(polygon, brush); + canvas.Fill(brush, polygon); } using Image defaultImage = new(384, 256); @@ -228,7 +229,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(path, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, path); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -291,7 +292,7 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput canvas) => canvas.Fill(polygon, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); @@ -347,7 +348,7 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput foreground = provider.GetImage(); Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(polygon, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); using Image baseImage = provider.GetImage(); using Image defaultImage = baseImage.Clone(); @@ -458,7 +459,7 @@ public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutpu void DrawAction(DrawingCanvas canvas) { canvas.Clear(clearBrush); - canvas.Fill(polygon, brush); + canvas.Fill(brush, polygon); } using Image defaultImage = provider.GetImage(); @@ -504,7 +505,7 @@ void DrawAction(DrawingCanvas canvas) canvas.Clear(clearBrush); using DrawingCanvas regionCanvas = canvas.CreateRegion(region); - regionCanvas.Fill(localPolygon, brush); + regionCanvas.Fill(brush, localPolygon); } using Image defaultImage = provider.GetImage(); @@ -1204,7 +1205,7 @@ void DrawAction(DrawingCanvas canvas) { float x = 20 + (i * 24); float y = 20 + (i * 22); - canvas.Fill(new RectangularPolygon(x, y, 80, 60), brush); + canvas.Fill(brush, new RectangularPolygon(x, y, 80, 60)); } } @@ -1273,7 +1274,7 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< IPath path = pathBuilder.Build(); Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(path, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, path); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1317,7 +1318,7 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid // Large polygon spanning most of the image to exercise many tiles. Brush brush = Brushes.Solid(Color.Black); EllipsePolygon ellipse = new(new PointF(400, 300), new SizeF(700, 500)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1363,13 +1364,13 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using Image defaultImage = provider.GetImage(); using (DrawingCanvas canvas1 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) { - canvas1.Fill(rect1, redBrush); + canvas1.Fill(redBrush, rect1); canvas1.Flush(); } using (DrawingCanvas canvas2 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) { - canvas2.Fill(rect2, blueBrush); + canvas2.Fill(blueBrush, rect2); canvas2.Flush(); } @@ -1381,13 +1382,13 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using (DrawingCanvas canvas1 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) { - canvas1.Fill(rect1, redBrush); + canvas1.Fill(redBrush, rect1); canvas1.Flush(); } using (DrawingCanvas canvas2 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) { - canvas2.Fill(rect2, blueBrush); + canvas2.Fill(blueBrush, rect2); canvas2.Flush(); } @@ -1423,14 +1424,14 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using (DrawingCanvas canvas1 = new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) { - canvas1.Fill(rect1, redBrush); + canvas1.Fill(redBrush, rect1); canvas1.Flush(); } using (DrawingCanvas canvas2 = new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) { - canvas2.Fill(rect2, blueBrush); + canvas2.Fill(blueBrush, rect2); canvas2.Flush(); } @@ -1478,7 +1479,7 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm new ColorStop(0.5F, Color.Green), new ColorStop(1, Color.Blue)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1523,7 +1524,7 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput new ColorStop(0, Color.Yellow), new ColorStop(1, Color.Purple)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1568,7 +1569,7 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1615,7 +1616,7 @@ public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1661,7 +1662,7 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(Test new ColorStop(0, Color.Cyan), new ColorStop(1, Color.Magenta)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1709,7 +1710,7 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma new ColorStop(0.67F, Color.Blue), new ColorStop(1, Color.Red)); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1755,7 +1756,7 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1795,7 +1796,7 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProv RectangularPolygon rect = new(16, 16, 224, 224); Brush brush = Brushes.Horizontal(Color.Black, Color.White); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1835,7 +1836,7 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(Test EllipsePolygon ellipse = new(128, 128, 100); Brush brush = Brushes.ForwardDiagonal(Color.DarkGreen, Color.LightGray); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(ellipse, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1875,7 +1876,7 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv RectangularPolygon rect = new(16, 16, 224, 224); Brush brush = new RecolorBrush(Color.Red, Color.Blue, 0.5F); - void DrawAction(DrawingCanvas canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1921,7 +1922,7 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1968,7 +1969,7 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput canvas) => canvas.Fill(rect, brush); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -1995,6 +1996,240 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 32); + + const string text = @"A long time ago in a galaxy +far, far away.... + +It is a period of civil war. +Rebel spaceships, striking +from a hidden base, have won +their first victory against +the evil Galactic Empire."; + + RichTextOptions textOptions = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Bottom, + TextAlignment = TextAlignment.Center, + Origin = new PointF(250, 360) + }; + + const float originX = 250; + const float originY = 380; + Matrix4x4 toOrigin = Matrix4x4.CreateTranslation(-originX, -originY, 0); + Matrix4x4 taperMatrix = Matrix4x4.Identity; + taperMatrix.M24 = -0.003F; + Matrix4x4 fromOrigin = Matrix4x4.CreateTranslation(originX, originY, 0); + Matrix4x4 perspective = toOrigin * taperMatrix * fromOrigin; + + DrawingOptions perspectiveOptions = new() { Transform = perspective }; + + // Star Destroyer geometry. + PointF[] sternFace = + [ + new(0, 0), new(300, 0), new(300, 80), new(0, 80), + ]; + + RectangularPolygon sternHighlightRect = new(4, 4, 292, 72); + + EllipsePolygon thrusterLeft = new(50, 40, 42, 42); + EllipsePolygon thrusterCenter = new(150, 40, 48, 48); + EllipsePolygon thrusterRight = new(250, 40, 42, 42); + + ProjectiveTransformBuilder transformBuilder = new(); + + Rectangle sternBounds = new(0, 0, 300, 80); + Matrix4x4 sternTransform = transformBuilder + .AppendQuadDistortion( + topLeft: new PointF(50, 80), + topRight: new PointF(400, 90), + bottomRight: new PointF(390, 135), + bottomLeft: new PointF(60, 140)) + .BuildMatrix(sternBounds); + + PointF[] bottomHull = + [ + new(0, 0), new(300, 0), new(150, 80), + ]; + + EllipsePolygon hullDome = new(117, 80, 96, 96); + + Rectangle hullBounds = new(0, 0, 300, 80); + Matrix4x4 hullTransform = transformBuilder.Clear() + .AppendQuadDistortion( + topLeft: new PointF(60, 140), + topRight: new PointF(390, 135), + bottomRight: new PointF(300, 160), + bottomLeft: new PointF(-30, 170)) + .BuildMatrix(hullBounds); + + PointF[] towerStem = + [ + new(14, 8), new(26, 8), new(26, 20), new(14, 20), + ]; + + PointF[] towerTop = + [ + new(0, 0), new(40, 0), new(40, 10), new(0, 10), + ]; + + Rectangle towerBounds = new(0, 0, 40, 20); + Matrix4x4 towerTransform = transformBuilder.Clear() + .AppendQuadDistortion( + topLeft: new PointF(175, 66), + topRight: new PointF(240, 68), + bottomRight: new PointF(238, 85), + bottomLeft: new PointF(177, 84)) + .BuildMatrix(towerBounds); + + Color sternColorLeft = Color.FromPixel(new Rgba32(70, 75, 85, 255)); + Color sternColorRight = Color.FromPixel(new Rgba32(35, 38, 45, 255)); + Color hullColorLeft = Color.FromPixel(new Rgba32(85, 90, 100, 255)); + Color hullColorRight = Color.FromPixel(new Rgba32(45, 50, 58, 255)); + Color highlightColorLeft = Color.FromPixel(new Rgba32(135, 140, 150, 255)); + Color highlightColorRight = Color.FromPixel(new Rgba32(65, 70, 80, 255)); + Color thrusterInnerGlow = Color.White; + Color thrusterOuterGlow = Color.Blue; + + LinearGradientBrush sternBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, sternColorLeft), + new ColorStop(1, sternColorRight)); + + LinearGradientBrush hullBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, hullColorLeft), + new ColorStop(1, hullColorRight)); + + LinearGradientBrush highlightBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + LinearGradientBrush towerBrush = new( + new PointF(0, 10), + new PointF(40, 10), + GradientRepetitionMode.None, + new ColorStop(0, sternColorLeft), + new ColorStop(1, sternColorRight)); + + LinearGradientBrush towerTopBrush = new( + new PointF(0, 5), + new PointF(40, 5), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + LinearGradientBrush domeBrush = new( + new PointF(21, 80), + new PointF(213, 80), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + EllipticGradientBrush thrusterBrushLeft = new( + new PointF(50, 40), + new PointF(50 + 42, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + EllipticGradientBrush thrusterBrushCenter = new( + new PointF(150, 40), + new PointF(150 + 48, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + EllipticGradientBrush thrusterBrushRight = new( + new PointF(250, 40), + new PointF(250 + 42, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + DrawingOptions sternOptions = new() { Transform = sternTransform }; + DrawingOptions hullOptions = new() { Transform = hullTransform }; + DrawingOptions towerOptions = new() { Transform = towerTransform }; + + void DrawAction(DrawingCanvas canvas) + { + // Bottom hull (draw first — behind stern). + canvas.Save(hullOptions); + canvas.Fill(highlightBrush, new Polygon(bottomHull)); + canvas.Restore(); + + // Stern face with thrusters, highlight, and dome. + canvas.Save(sternOptions); + canvas.Fill(domeBrush, hullDome); + canvas.Draw(Pens.Solid(highlightColorRight, 2), hullDome); + canvas.Fill(sternBrush, new Polygon(sternFace)); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), sternHighlightRect); + canvas.Fill(thrusterBrushLeft, thrusterLeft); + canvas.Fill(thrusterBrushCenter, thrusterCenter); + canvas.Fill(thrusterBrushRight, thrusterRight); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterLeft); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterCenter); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterRight); + canvas.Restore(); + + // Bridge tower. + canvas.Save(towerOptions); + canvas.Fill(towerTopBrush, new Polygon(towerTop)); + canvas.Fill(towerBrush, new Polygon(towerStem)); + canvas.Restore(); + + // Text crawl with perspective. + canvas.Save(perspectiveOptions); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Yellow), pen: null); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using Image cpuRegionImage = provider.GetImage(); + using WebGPUDrawingBackend cpuRegionBackend = new(); + RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendTriplet(provider, "StarWarsCrawl", defaultImage, cpuRegionImage, nativeSurfaceImage); + AssertCoverageExecutionAccounting(cpuRegionBackend); + AssertCoverageExecutionAccounting(nativeSurfaceBackend); + AssertGpuPathWhenRequired(cpuRegionBackend); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + } + private static Buffer2DRegion GetFrameRegion(Image image) where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index bbc825bc7..b18e869e3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -27,8 +27,8 @@ public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() Brush brushA = Brushes.Solid(Color.Red); Brush brushB = Brushes.Solid(Color.Blue); - canvas.Fill(path, brushA); - canvas.Fill(path, brushB); + canvas.Fill(brushA, path); + canvas.Fill(brushB, path); canvas.Flush(); Assert.True(backend.HasBatch); @@ -78,7 +78,7 @@ public void Flush_SamePathReusedMultipleTimes_BatchesCommands() for (int i = 0; i < 10; i++) { - canvas.Fill(path, Brushes.Solid(Color.FromPixel(new Rgba32((byte)i, 0, 0, 255)))); + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32((byte)i, 0, 0, 255))), path); } canvas.Flush(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs index 59bf3d9e0..6b8cb7095 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs @@ -34,9 +34,9 @@ public void Fill_WithGradientAndPatternBrushes_MatchesReference(TestImag Brush hatchBrush = Brushes.ForwardDiagonal(Color.DarkSlateGray.WithAlpha(0.7F), Color.Transparent); canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(14, 14, 176, 126), linearBrush); - canvas.Fill(new EllipsePolygon(new PointF(236, 90), new SizeF(132, 98)), radialBrush); - canvas.Fill(CreateClosedPathBuilder(), hatchBrush); + canvas.Fill(linearBrush, new Rectangle(14, 14, 176, 126)); + canvas.Fill(radialBrush, new EllipsePolygon(new PointF(236, 90), new SizeF(132, 98))); + canvas.Fill(hatchBrush, CreateClosedPathBuilder()); canvas.Draw(Pens.DashDot(Color.Black, 3), new Rectangle(10, 10, 300, 180)); canvas.Flush(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs index 7cdab4b14..8ce866531 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs @@ -17,12 +17,12 @@ public void Clear_RegionAndPath_MatchesReference(TestImageProvider canvas = CreateCanvas(provider, target, new DrawingOptions()); canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); - canvas.Fill(new Rectangle(22, 16, 188, 118), Brushes.Solid(Color.Crimson.WithAlpha(0.8F))); + canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.8F)), new Rectangle(22, 16, 188, 118)); canvas.DrawEllipse(Pens.Solid(Color.Gold, 5), new PointF(128, 80), new SizeF(140, 90)); - canvas.Clear(new Rectangle(56, 36, 108, 64), Brushes.Solid(Color.LightYellow.WithAlpha(0.45F))); + canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.45F)), new Rectangle(56, 36, 108, 64)); IPath clearPath = new EllipsePolygon(new PointF(178, 80), new SizeF(74, 56)); - canvas.Clear(clearPath, Brushes.Solid(Color.Transparent)); + canvas.Clear(Brushes.Solid(Color.Transparent), clearPath); canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(10, 10, 236, 140)); canvas.Flush(); @@ -40,17 +40,17 @@ public void Clear_WithClipPath_MatchesReference(TestImageProvider canvas = CreateCanvas(provider, target, new DrawingOptions()); canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(0, 0, 320, 200), Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); - canvas.Fill(new Rectangle(26, 18, 268, 164), Brushes.Solid(Color.Crimson.WithAlpha(0.78F))); + canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F)), new Rectangle(0, 0, 320, 200)); + canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.78F)), new Rectangle(26, 18, 268, 164)); canvas.DrawEllipse(Pens.Solid(Color.Gold, 5F), new PointF(160, 100), new SizeF(196, 116)); IPath clipPath = new EllipsePolygon(new PointF(160, 100), new SizeF(214, 126)); _ = canvas.Save(new DrawingOptions(), clipPath); canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.85F))); - canvas.Clear(new Rectangle(40, 24, 108, 72), Brushes.Solid(Color.MediumPurple.WithAlpha(0.72F))); - canvas.Clear(new Rectangle(172, 96, 110, 70), Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F))); - canvas.Clear(new EllipsePolygon(new PointF(164, 98), new SizeF(74, 48)), Brushes.Solid(Color.Transparent)); + canvas.Clear(Brushes.Solid(Color.MediumPurple.WithAlpha(0.72F)), new Rectangle(40, 24, 108, 72)); + canvas.Clear(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(172, 96, 110, 70)); + canvas.Clear(Brushes.Solid(Color.Transparent), new EllipsePolygon(new PointF(164, 98), new SizeF(74, 48))); canvas.Restore(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs index 07f4e00ed..52c902fc3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs @@ -20,7 +20,7 @@ public void DrawImage_WithRotationTransform_MatchesReference(TestImagePr DrawingOptions options = new() { - Transform = Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F))) }; using DrawingCanvas canvas = CreateCanvas(provider, target, options); @@ -75,13 +75,13 @@ public void DrawImage_WithClipPathAndTransform_MatchesReference(TestImag DrawingOptions transformedOptions = new() { - Transform = Matrix3x2.CreateRotation(0.32F, new Vector2(180, 120)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.32F, new Vector2(180, 120))) }; IPath clipPath = new EllipsePolygon(new PointF(180, 120), new SizeF(208, 126)); canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(18, 16, 324, 208), Brushes.Solid(Color.LightGray.WithAlpha(0.45F))); + canvas.Fill(Brushes.Solid(Color.LightGray.WithAlpha(0.45F)), new Rectangle(18, 16, 324, 208)); _ = canvas.Save(transformedOptions, clipPath); canvas.DrawImage( diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs index 84eb2fd0a..f369fe93e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs @@ -18,7 +18,7 @@ public void Draw_PathBuilder_MatchesReference(TestImageProvider DrawingOptions options = new() { - Transform = Matrix3x2.CreateRotation(-0.15F, new Vector2(96F, 64F)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(-0.15F, new Vector2(96F, 64F))) }; using DrawingCanvas canvas = CreateCanvas(provider, target, options); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs index 5d684ce85..de5d52f87 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs @@ -18,14 +18,14 @@ public void Fill_PathBuilder_MatchesReference(TestImageProvider DrawingOptions options = new() { - Transform = Matrix3x2.CreateRotation(0.2F, new Vector2(96F, 64F)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.2F, new Vector2(96F, 64F))) }; using DrawingCanvas canvas = CreateCanvas(provider, target, options); PathBuilder pathBuilder = CreateClosedPathBuilder(); canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(pathBuilder, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), pathBuilder); canvas.Flush(); target.DebugSave(provider, appendSourceFileOrDescription: false); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs index e454e4a78..95afcbf46 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs @@ -30,14 +30,14 @@ public void Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference( }; canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(12, 12, 336, 196), Brushes.Solid(Color.AliceBlue.WithAlpha(0.7F))); + canvas.Fill(Brushes.Solid(Color.AliceBlue.WithAlpha(0.7F)), new Rectangle(12, 12, 336, 196)); _ = canvas.Save(evenOddOptions); - canvas.Fill(leftPath, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), leftPath); canvas.Restore(); _ = canvas.Save(nonZeroOptions); - canvas.Fill(rightPath, Brushes.Solid(Color.DeepPink.WithAlpha(0.85F))); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), rightPath); canvas.Restore(); canvas.Draw(Pens.Solid(Color.Black, 3F), leftPath); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs index 435069a8a..1cd08602f 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -22,7 +22,7 @@ public void CreateRegion_LocalCoordinates_MatchesReference(TestImageProv using (DrawingCanvas regionCanvas = canvas.CreateRegion(new Rectangle(40, 24, 140, 96))) { - regionCanvas.Fill(new Rectangle(10, 8, 80, 46), Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F))); + regionCanvas.Fill(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(10, 8, 80, 46)); regionCanvas.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 140, 96)); regionCanvas.DrawLine( Pens.Solid(Color.OrangeRed, 4), @@ -49,12 +49,12 @@ public void SaveRestore_ClipPath_MatchesReference(TestImageProvider(TestImageProvider< DrawingOptions firstOptions = new() { - Transform = Matrix3x2.CreateTranslation(20F, 12F) + Transform = Matrix4x4.CreateTranslation(20F, 12F, 0) }; int firstSaveCount = canvas.Save(firstOptions, new RectangularPolygon(20, 20, 144, 104)); - canvas.Fill(new Rectangle(0, 0, 120, 84), Brushes.Solid(Color.SkyBlue.WithAlpha(0.8F))); + canvas.Fill(Brushes.Solid(Color.SkyBlue.WithAlpha(0.8F)), new Rectangle(0, 0, 120, 84)); DrawingOptions secondOptions = new() { - Transform = Matrix3x2.CreateRotation(0.24F, new Vector2(112, 80)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.24F, new Vector2(112, 80))) }; _ = canvas.Save(secondOptions, new EllipsePolygon(new PointF(112, 80), new SizeF(130, 90))); @@ -99,7 +99,7 @@ public void RestoreTo_MultipleStates_MatchesReference(TestImageProvider< new PointF(168, 92)); canvas.RestoreTo(1); - canvas.Fill(new Rectangle(156, 106, 48, 34), Brushes.Solid(Color.Gold.WithAlpha(0.7F))); + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.7F)), new Rectangle(156, 106, 48, 34)); canvas.Draw(Pens.Solid(Color.DarkSlateGray, 4), new Rectangle(8, 8, 208, 144)); canvas.Flush(); @@ -117,11 +117,11 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(12, 12, 296, 196), Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F))); + canvas.Fill(Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F)), new Rectangle(12, 12, 296, 196)); DrawingOptions rootOptions = new() { - Transform = Matrix3x2.CreateTranslation(6F, 4F) + Transform = Matrix4x4.CreateTranslation(6F, 4F, 0) }; IPath rootClip = new EllipsePolygon(new PointF(160, 110), new SizeF(252, 164)); @@ -129,16 +129,16 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference using (DrawingCanvas outerRegion = canvas.CreateRegion(new Rectangle(30, 24, 240, 156))) { - outerRegion.Fill(new Rectangle(0, 0, 240, 156), Brushes.Solid(Color.LightBlue.WithAlpha(0.35F))); + outerRegion.Fill(Brushes.Solid(Color.LightBlue.WithAlpha(0.35F)), new Rectangle(0, 0, 240, 156)); outerRegion.Draw(Pens.Solid(Color.DarkBlue, 3F), new Rectangle(0, 0, 240, 156)); DrawingOptions outerOptions = new() { - Transform = Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78))) }; _ = outerRegion.Save(outerOptions, new RectangularPolygon(18, 14, 204, 128)); - outerRegion.Fill(new Rectangle(16, 16, 208, 124), Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F))); + outerRegion.Fill(Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F)), new Rectangle(16, 16, 208, 124)); using (DrawingCanvas innerRegion = outerRegion.CreateRegion(new Rectangle(52, 34, 132, 82))) { @@ -146,11 +146,11 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference DrawingOptions innerOptions = new() { - Transform = Matrix3x2.CreateSkew(0.18F, 0F) + Transform = new Matrix4x4(Matrix3x2.CreateSkew(0.18F, 0F)) }; _ = innerRegion.Save(innerOptions, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); - innerRegion.Fill(new Rectangle(0, 0, 132, 82), Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F))); + innerRegion.Fill(Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F)), new Rectangle(0, 0, 132, 82)); innerRegion.DrawLine( Pens.Solid(Color.DarkRed, 4F), new PointF(0, 80), @@ -163,7 +163,7 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference outerRegion.Restore(); - outerRegion.Fill(new Rectangle(8, 112, 90, 30), Brushes.Solid(Color.OrangeRed.WithAlpha(0.6F))); + outerRegion.Fill(Brushes.Solid(Color.OrangeRed.WithAlpha(0.6F)), new Rectangle(8, 112, 90, 30)); outerRegion.DrawLine(Pens.Solid(Color.Black, 3F), new PointF(8, 8), new PointF(232, 148)); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs index c23d3aa99..a6d0411ca 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -30,7 +30,7 @@ public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider }; canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(12, 12, 336, 196), Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F))); + canvas.Fill(Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F)), new Rectangle(12, 12, 336, 196)); _ = canvas.Save(evenOddOptions); canvas.Draw(pen, leftPath); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs index f41010552..ed523dff9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -60,7 +60,7 @@ public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(Te DrawingOptions options = new() { - Transform = Matrix3x2.CreateTranslation(24F, 22F) + Transform = Matrix4x4.CreateTranslation(24F, 22F, 0) }; using DrawingCanvas canvas = CreateCanvas(provider, target, options); @@ -77,7 +77,7 @@ public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(Te }; canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(0, 0, 712, 276), Brushes.Solid(Color.LightSteelBlue.WithAlpha(0.25F))); + canvas.Fill(Brushes.Solid(Color.LightSteelBlue.WithAlpha(0.25F)), new Rectangle(0, 0, 712, 276)); canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text); @@ -120,7 +120,7 @@ public void DrawText_FillAndStroke_MatchesReference(TestImageProvider canvas = CreateCanvas(provider, target, options); @@ -159,7 +159,7 @@ public void DrawText_PenOnly_MatchesReference(TestImageProvider }; canvas.Clear(Brushes.Solid(Color.White)); - canvas.Fill(new Rectangle(12, 14, 296, 152), Brushes.Solid(Color.LightSkyBlue.WithAlpha(0.45F))); + canvas.Fill(Brushes.Solid(Color.LightSkyBlue.WithAlpha(0.45F)), new Rectangle(12, 14, 296, 152)); canvas.DrawText(textOptions, "OUTLINE", brush: null, pen: Pens.Solid(Color.SeaGreen, 3.5F)); canvas.Flush(); @@ -227,7 +227,7 @@ public void DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference(TestImageProv float endX = startX + metrics.Extent; canvas.Fill( - new RectangularPolygon(startX, lineOriginY, endX - startX, metrics.LineHeight), - Brushes.Solid(bandColors[i % bandColors.Length])); + Brushes.Solid(bandColors[i % bandColors.Length]), + new RectangularPolygon(startX, lineOriginY, endX - startX, metrics.LineHeight)); canvas.DrawLine( Pens.Solid(Color.Teal.WithAlpha(0.9F), 1.5F), @@ -102,8 +102,8 @@ public void TextMeasuring_RenderedMetrics_MatchesReference(TestImageProv { FontRectangle b = charBounds[i].Bounds; canvas.Fill( - new RectangularPolygon(b.X, b.Y, b.Width, b.Height), - Brushes.Solid(charColors[i % charColors.Length])); + Brushes.Solid(charColors[i % charColors.Length]), + new RectangularPolygon(b.X, b.Y, b.Width, b.Height)); } } @@ -168,17 +168,17 @@ public void TextMeasuring_RenderedMetrics_MatchesReference(TestImageProv if (keyEntries[i].Color2 is Color c2) { canvas.Fill( - new RectangularPolygon(x, y, halfW, swatchH), - Brushes.Solid(keyEntries[i].Color1)); + Brushes.Solid(keyEntries[i].Color1), + new RectangularPolygon(x, y, halfW, swatchH)); canvas.Fill( - new RectangularPolygon(x + halfW, y, halfW, swatchH), - Brushes.Solid(c2)); + Brushes.Solid(c2), + new RectangularPolygon(x + halfW, y, halfW, swatchH)); } else { canvas.Fill( - new RectangularPolygon(x, y, swatchW, swatchH), - Brushes.Solid(keyEntries[i].Color1)); + Brushes.Solid(keyEntries[i].Color1), + new RectangularPolygon(x, y, swatchW, swatchH)); } } else diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs index 284fb83f1..360d02740 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs @@ -19,7 +19,7 @@ public void Fill_AliasedWithDefaultThreshold(TestImageProvider p DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; using Image image = provider.GetImage(); - image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); int whitePixels = CountPixelsAbove(image, 250); int partialPixels = CountPixelsBetween(image, 1, 250); @@ -47,11 +47,11 @@ public void Fill_AliasedLowThreshold_ProducesMorePixelsThanHighThreshold }; using Image lowImage = provider.GetImage(); - lowImage.Mutate(ctx => ctx.ProcessWithCanvas(lowOptions, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + lowImage.Mutate(ctx => ctx.ProcessWithCanvas(lowOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); int lowCount = CountPixelsAbove(lowImage, 250); using Image highImage = provider.GetImage(); - highImage.Mutate(ctx => ctx.ProcessWithCanvas(highOptions, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + highImage.Mutate(ctx => ctx.ProcessWithCanvas(highOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); int highCount = CountPixelsAbove(highImage, 250); // A lower threshold includes more edge pixels, so the fill area should be larger. @@ -76,10 +76,10 @@ public void Fill_AntialiasedIgnoresThreshold(TestImageProvider p }; using Image image1 = provider.GetImage(); - image1.Mutate(ctx => ctx.ProcessWithCanvas(options1, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + image1.Mutate(ctx => ctx.ProcessWithCanvas(options1, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); using Image image2 = provider.GetImage(); - image2.Mutate(ctx => ctx.ProcessWithCanvas(options2, canvas => canvas.Fill(circle, Brushes.Solid(Color.White)))); + image2.Mutate(ctx => ctx.ProcessWithCanvas(options2, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); // In antialiased mode the threshold is irrelevant; images should be identical. ImageComparer.Exact.VerifySimilarity(image1, image2); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs index 50ea36b3b..be13d9094 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs @@ -39,12 +39,12 @@ public void BlendingsDarkBlueRectBlendHotPinkRect( image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); })); image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => { - canvas.Fill(new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)); })); VerifyImage(provider, blending, composition, image); @@ -66,17 +66,17 @@ public void BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); })); image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => { - canvas.Fill(new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)); })); image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => { - canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(Color.Transparent)); + canvas.Fill(Brushes.Solid(Color.Transparent), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); })); VerifyImage(provider, blending, composition, image); @@ -100,17 +100,17 @@ public void BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse< image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { // Keep legacy shape coordinates identical to the original test. - canvas.Fill(new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY)); })); image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => { - canvas.Fill(new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY), Brushes.Solid(Color.HotPink)); + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY)); })); image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => { - canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(transparentRed)); + canvas.Fill(Brushes.Solid(transparentRed), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); })); VerifyImage(provider, blending, composition, image); @@ -134,12 +134,12 @@ public void BlendingsDarkBlueRectBlendBlackEllipse( destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY), Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); })); sourceImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY), Brushes.Solid(Color.Black)); + canvas.Fill(Brushes.Solid(Color.Black), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); })); destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs index 95d55b8c3..c837a76c9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs @@ -100,7 +100,7 @@ public void Clear_Region(TestImageProvider provider, int x0, int Rectangle region = new(x0, y0, w, h); DrawingOptions options = new(); - image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(region, Brushes.Solid(clearColor)))); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(clearColor), region))); image.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); AssertRegionFill(image, region, clearColor, backgroundColor); @@ -127,7 +127,7 @@ public void Clear_Region_WorksOnWrappedMemoryImage( Rectangle region = new(x0, y0, w, h); DrawingOptions options = new(); - wrapped.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(region, Brushes.Solid(clearColor)))); + wrapped.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(clearColor), region))); wrapped.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); AssertRegionFill(wrapped, region, clearColor, backgroundColor); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs index ba812bb5e..2f680d51e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs @@ -27,7 +27,7 @@ public void ClipOffset(TestImageProvider provider, float dx, flo Rectangle bounds = canvas.Bounds; int outerRadii = (int)(Math.Min(bounds.Width, bounds.Height) * sizeMult); Star star = new(new PointF(bounds.Width / 2F, bounds.Height / 2F), 5, outerRadii / 2F, outerRadii); - Matrix3x2 builder = Matrix3x2.CreateTranslation(new Vector2(dx, dy)); + Matrix4x4 builder = Matrix4x4.CreateTranslation(dx, dy, 0); canvas.Process(star.Transform(builder), ctx => ctx.DetectEdges()); }), testOutputDetails: testDetails, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs index 4a0911c25..6cb0e4424 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs @@ -32,7 +32,7 @@ public void FillOutsideBoundsDrawRectactangleOutsideBoundsDrawingArea(int xpos) using Image image = new(width, height, Color.Red.ToPixel()); Rectangle rectangle = new(xpos, 0, width, height); - image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(rectangle, Brushes.Solid(Color.Black)))); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Black), rectangle))); } [Theory] @@ -45,7 +45,7 @@ public void FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea(TestImageProvide EllipsePolygon circle = new(xpos, ypos, width, height); provider.RunValidatingProcessorTest( - ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(circle, Brushes.Solid(Color.Black))), + ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Black), circle)), $"({xpos}_{ypos})", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs index 2d4375a02..d9e92df9c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs @@ -53,10 +53,10 @@ public void FillPathSVGArcs(TestImageProvider provider) using Image image = provider.GetImage(); image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(path, Brushes.Solid(Color.Green)); - canvas.Fill(path2, Brushes.Solid(Color.Red)); - canvas.Fill(path3, Brushes.Solid(Color.Purple)); - canvas.Fill(path4, Brushes.Solid(Color.Blue)); + canvas.Fill(Brushes.Solid(Color.Green), path); + canvas.Fill(Brushes.Solid(Color.Red), path2); + canvas.Fill(Brushes.Solid(Color.Purple), path3); + canvas.Fill(Brushes.Solid(Color.Blue), path4); })); image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); @@ -103,7 +103,7 @@ public void FillPathCanvasArcs(TestImageProvider provider) if (i > 1) { - canvas.Fill(pb.Build(), Brushes.Solid(Color.Black)); + canvas.Fill(Brushes.Solid(Color.Black), pb.Build()); } else { @@ -141,8 +141,8 @@ public void FillPathArcToAlternates(TestImageProvider provider) using Image image = provider.GetImage(); image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(path, Brushes.Solid(Color.Yellow)); - canvas.Fill(path2, Brushes.Solid(Color.Red.WithAlpha(.5F))); + canvas.Fill(Brushes.Solid(Color.Yellow), path); + canvas.Fill(Brushes.Solid(Color.Red.WithAlpha(.5F)), path2); })); image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs index 77f44a164..1d695bffc 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs @@ -106,7 +106,7 @@ public void FillSolidBrush_Region(TestImageProvider provider, in Color color = Color.Blue; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(region, Brushes.Solid(color))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), region)), testDetails, ImageComparer.Exact); } @@ -127,7 +127,7 @@ public void FillSolidBrush_Region_WorksOnWrappedMemoryImage( Color color = Color.Blue; provider.RunValidatingProcessorTestOnWrappedMemoryImage( - c => c.ProcessWithCanvas(canvas => canvas.Fill(region, Brushes.Solid(color))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), region)), testDetails, ImageComparer.Exact, useReferenceOutputFrom: nameof(this.FillSolidBrush_Region)); @@ -161,7 +161,7 @@ public void FillSolidBrush_BlendFillColorOverBackground( if (triggerFillRegion) { RectangularPolygon path = new(0, 0, 16, 16); - image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(path, Brushes.Solid(fillColor)))); + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(fillColor), path))); } else { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs index d6b348d21..d1a4253e4 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs @@ -18,8 +18,8 @@ public void FillImageBrushDoesNotDisposeImage() ImageBrush brush = new(source); using (Image destination = new(10, 10)) { - destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(new Rectangle(0, 0, 10, 10), brush))); - destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(new Rectangle(0, 0, 10, 10), brush))); + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush, new Rectangle(0, 0, 10, 10)))); + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush, new Rectangle(0, 0, 10, 10)))); } } } @@ -90,8 +90,8 @@ public void FillImageBrushCanOffsetImage(TestImageProvider provi ImageBrush brush = new(overlay); background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new Rectangle(0, 0, 400, 200), brush); - canvas.Fill(new Rectangle(-100, 200, 500, 200), brush); + canvas.Fill(brush, new Rectangle(0, 0, 400, 200)); + canvas.Fill(brush, new Rectangle(-100, 200, 500, 200)); })); background.DebugSave(provider, appendSourceFileOrDescription: false); @@ -112,8 +112,8 @@ public void FillImageBrushCanOffsetViaBrushImage(TestImageProvider ctx.ProcessWithCanvas(canvas => { - canvas.Fill(new Rectangle(0, 0, 400, 200), brush); - canvas.Fill(new Rectangle(0, 200, 400, 200), brushOffset); + canvas.Fill(brush, new Rectangle(0, 0, 400, 200)); + canvas.Fill(brushOffset, new Rectangle(0, 200, 400, 200)); })); background.DebugSave(provider, appendSourceFileOrDescription: false); @@ -194,13 +194,13 @@ private static void FillImageBrushDrawFull( if (half) { int halfWidth = size.Width / 2; - canvas.Fill(new Rectangle(x, y, halfWidth, size.Height), halfBrush); + canvas.Fill(halfBrush, new Rectangle(x, y, halfWidth, size.Height)); x += halfWidth; half = false; } else { - canvas.Fill(new Rectangle(x, y, size.Width, size.Height), brush); + canvas.Fill(brush, new Rectangle(x, y, size.Width, size.Height)); x += size.Width; } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs index 834ad6f61..fde0f27f1 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs @@ -48,8 +48,8 @@ public void FillPolygon_Solid_Basic(TestImageProvider provider, provider.RunValidatingProcessorTest( c => c.ProcessWithCanvas(options, canvas => { - canvas.Fill(shape1, Brushes.Solid(Color.White)); - canvas.Fill(shape2, Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.White), shape1); + canvas.Fill(Brushes.Solid(Color.White), shape2); }), testOutputDetails: $"aa{antialias}", appendPixelTypeToFileName: false, @@ -79,7 +79,7 @@ public void FillPolygon_Solid(TestImageProvider provider, string FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(color))), + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), polygon)), outputDetails, appendSourceFileOrDescription: false); } @@ -96,11 +96,11 @@ public void FillPolygon_Solid_Transformed(TestImageProvider prov Polygon polygon = new(new LinearLineSegment(simplePath)); DrawingOptions options = new() { - Transform = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200)) + Transform = new Matrix4x4(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) }; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); } [Theory] @@ -111,11 +111,11 @@ public void FillPolygon_RectangularPolygon_Solid_Transformed(TestImagePr RectangularPolygon polygon = new(25, 25, 50, 50); DrawingOptions options = new() { - Transform = Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) }; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); } [Theory] @@ -126,11 +126,11 @@ public void FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White)))); + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); } [Theory] @@ -157,7 +157,7 @@ public void FillPolygon_Complex(TestImageProvider provider, bool }; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(Color.White))), + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon)), testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, @@ -188,7 +188,7 @@ public void FillPolygon_Concave(TestImageProvider provider, bool Color color = Color.LightGreen; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), testOutputDetails: $"Reverse({reverse})", comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, @@ -204,7 +204,7 @@ public void FillPolygon_StarCircle(TestImageProvider provider) IPath shape = circle.Clip(star); provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(shape, Brushes.Solid(Color.White))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White), shape)), comparer: ImageComparer.TolerantPercentage(0.01f), appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -228,9 +228,9 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi provider.RunValidatingProcessorTest( c => c.ProcessWithCanvas(options, canvas => { - canvas.Fill(circle, Brushes.Solid(Color.DeepPink)); - canvas.Fill(star, Brushes.Solid(Color.LightGray)); - canvas.Fill(shape, Brushes.Solid(Color.ForestGreen)); + canvas.Fill(Brushes.Solid(Color.DeepPink), circle); + canvas.Fill(Brushes.Solid(Color.LightGray), star); + canvas.Fill(Brushes.Solid(Color.ForestGreen), shape); }), testOutputDetails: operation.ToString(), comparer: ImageComparer.TolerantPercentage(0.01F), @@ -251,7 +251,7 @@ public void FillPolygon_Pattern(TestImageProvider provider) PatternBrush brush = Brushes.Horizontal(Color.Yellow); provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), appendSourceFileOrDescription: false); } @@ -271,7 +271,7 @@ public void FillPolygon_ImageBrush(TestImageProvider provider, s ImageBrush brush = new(brushImage); provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), System.IO.Path.GetFileNameWithoutExtension(brushImageName), appendSourceFileOrDescription: false); } @@ -298,7 +298,7 @@ public void FillPolygon_ImageBrush_Rect(TestImageProvider provid ImageBrush brush = new(brushImage, new RectangleF(left, top, width, height)); provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, brush)), + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect", appendSourceFileOrDescription: false); } @@ -312,7 +312,7 @@ public void FillPolygon_RectangularPolygon(TestImageProvider pro Color color = Color.White; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), appendSourceFileOrDescription: false); } @@ -331,7 +331,7 @@ public void FillPolygon_RegularPolygon(TestImageProvider provide FormattableString testOutput = $"V({vertices})_R({radius})_Ang({angleDeg})"; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(canvas => canvas.Fill(polygon, Brushes.Solid(color))), + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), testOutput, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -355,7 +355,7 @@ public void FillPolygon_EllipsePolygon(TestImageProvider provide }; provider.RunValidatingProcessorTest( - c => c.ProcessWithCanvas(options, canvas => canvas.Fill(polygon, Brushes.Solid(color))), + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), polygon)), testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -385,7 +385,7 @@ public void FillPolygon_IntersectionRules_OddEven(TestImageProvider c.ProcessWithCanvas(options, canvas => canvas.Fill(poly, Brushes.Solid(Color.HotPink)))); + img.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly))); provider.Utility.SaveTestOutputFile(img); Assert.Equal(Color.Blue.ToPixel(), img[25, 25]); @@ -416,7 +416,7 @@ public void FillPolygon_IntersectionRules_Nonzero(TestImageProvider c.ProcessWithCanvas(options, canvas => canvas.Fill(poly, Brushes.Solid(Color.HotPink)))); + img.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly))); provider.Utility.SaveTestOutputFile(img); Assert.Equal(Color.HotPink.ToPixel(), img[25, 25]); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 83ecd7485..35c03cc03 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -83,7 +83,7 @@ public void SolidBezierFilledBezier(TestImageProvider provider) ctx => ctx.ProcessWithCanvas(canvas => { canvas.Clear(Brushes.Solid(Color.Blue)); - canvas.Fill(polygon, brush); + canvas.Fill(brush, polygon); })); } @@ -107,7 +107,7 @@ public void SolidBezierOverlayByFilledPolygonOpacity(TestImageProvider ctx.ProcessWithCanvas(canvas => { canvas.Clear(Brushes.Solid(Color.Blue)); - canvas.Fill(polygon, brush); + canvas.Fill(brush, polygon); })); } @@ -374,7 +374,7 @@ public void FillComplexPolygon_SolidFill(TestImageProvider provi DrawingOptions options = new(); using Image image = provider.GetImage(); - image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(clipped, Brushes.Solid(color)))); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), clipped))); image.DebugSave( provider, testDetails, @@ -438,10 +438,10 @@ public void DrawPolygon_Transformed(TestImageProvider provider) IPath polygon = new Polygon(new LinearLineSegment(simplePath)); DrawingOptions options = new() { - Transform = Matrix3x2.CreateSkew( + Transform = new Matrix4x4(Matrix3x2.CreateSkew( GeometryUtilities.DegreeToRadian(-15), 0, - new Vector2(200, 200)) + new Vector2(200, 200))) }; using Image image = provider.GetImage(); @@ -458,7 +458,7 @@ public void DrawPolygonRectangular_Transformed(TestImageProvider RectangularPolygon polygon = new(25, 25, 50, 50); DrawingOptions options = new() { - Transform = Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50)) + Transform = new Matrix4x4(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) }; using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs index 328154e13..c8adc0674 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs @@ -40,7 +40,7 @@ public void RecolorImage_InBox(TestImageProvider provider, strin { Rectangle bounds = canvas.Bounds; Rectangle region = new(0, (bounds.Height / 2) - (bounds.Height / 4), bounds.Width, bounds.Height / 2); - canvas.Fill(region, brush); + canvas.Fill(brush, region); }), testInfo); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs index cd047bfe6..0bd59b8c4 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs @@ -40,7 +40,7 @@ public void CompareToSkiaResults_StarCircle(TestImageProvider provider) private static void CompareToSkiaResultsImpl(TestImageProvider provider, IPath shape) { using Image image = provider.GetImage(); - image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(shape, Brushes.Solid(Color.White)))); + image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White), shape))); image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); using SKBitmap bitmap = new(new SKImageInfo(image.Width, image.Height)); @@ -82,7 +82,7 @@ public void LargeGeoJson_Lines(TestImageProvider provider, string geoJso { string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); - PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(sx, sy)); + PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix4x4.CreateScale(sx, sy, 1)); using Image image = provider.GetImage(); DrawingOptions options = new() @@ -123,7 +123,7 @@ private static Image FillGeoJsonPolygons(TestImageProvider provi { string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); - PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(pixelOffset)); + PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix4x4.CreateScale(scale.X, scale.Y, 1) * Matrix4x4.CreateTranslation(pixelOffset.X, pixelOffset.Y, 0)); Image image = provider.GetImage(); DrawingOptions options = new() @@ -140,7 +140,7 @@ private static Image FillGeoJsonPolygons(TestImageProvider provi rnd.NextBytes(rgb); Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); - canvas.Fill(new Polygon(new LinearLineSegment(loop)), Brushes.Solid(color)); + canvas.Fill(Brushes.Solid(color), new Polygon(new LinearLineSegment(loop))); } })); @@ -158,9 +158,9 @@ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, i Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60) - * Matrix3x2.CreateTranslation(pixelOffset, pixelOffset); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1) + * Matrix4x4.CreateTranslation(pixelOffset, pixelOffset, 0); IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); @@ -193,8 +193,8 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); @@ -204,9 +204,9 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi { foreach (PointF[] loop in points) { - IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); - outline = outline.Transform(Matrix3x2.CreateScale(scale, scale)); - canvas.Fill(outline, pen.StrokeFill); + IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix4x4.CreateTranslation(0.5F, 0.5F, 0))); + outline = outline.Transform(Matrix4x4.CreateScale(scale, scale, 1)); + canvas.Fill(pen.StrokeFill, outline); } })); @@ -230,9 +230,9 @@ public void Missisippi_Skia(int offset) Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60) - * Matrix3x2.CreateTranslation(offset, offset); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1) + * Matrix4x4.CreateTranslation(offset, offset, 0); IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); SKPath path = new(); @@ -284,7 +284,7 @@ public void LargeGeoJson_States_Separate_Benchmark(TestImageProvider pro Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) * Matrix4x4.CreateScale(60, 60, 1); IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); @@ -311,7 +311,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) * Matrix3x2.CreateScale(60, 60); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) * Matrix4x4.CreateScale(60, 60, 1); IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); PathBuilder pb = new(); @@ -336,7 +336,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider public void LargeStar_Benchmark(TestImageProvider provider, int thickness) { List points = CreateStarPolygon(1001, 100F); - Matrix3x2 transform = Matrix3x2.CreateTranslation(250, 250); + Matrix4x4 transform = Matrix4x4.CreateTranslation(250, 250, 0); DrawingOptions options = new() { Transform = transform }; using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs index e06c7b184..19837768a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs @@ -208,7 +208,7 @@ public void FontShapesAreRenderedCorrectly_WithRotationApplied( Origin = new PointF(x, y) }; - Matrix3x2 transform = Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY)); + Matrix4x4 transform = new(Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY))); DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( @@ -253,7 +253,7 @@ public void FontShapesAreRenderedCorrectly_WithSkewApplied( Origin = new PointF(x, y) }; - Matrix3x2 transform = Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY)); + Matrix4x4 transform = new(Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY))); DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( @@ -482,8 +482,8 @@ public void CanRotateFilledFont_Issue175( AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); RichTextOptions textOptions = new(font); - FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); - Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); + FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(text, textOptions); + Matrix4x4 transform = new(builder.BuildMatrix(Rectangle.Round(new RectangleF(renderable.X, renderable.Y, renderable.Width, renderable.Height)))); DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( @@ -514,8 +514,8 @@ public void CanRotateOutlineFont_Issue175( AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); RichTextOptions textOptions = new(font); - FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); - Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); + FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(text, textOptions); + Matrix4x4 transform = new(builder.BuildMatrix(Rectangle.Round(new RectangleF(renderable.X, renderable.Y, renderable.Width, renderable.Height)))); DrawingOptions drawingOptions = new() { Transform = transform }; provider.RunValidatingProcessorTest( diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs index 383927e1e..858301d64 100644 --- a/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/DefaultRasterizerTests.cs @@ -29,7 +29,7 @@ public void MatchesDefaultRasterizer_ForLargeSelfIntersectingPath(IntersectionRu (2, 5), (2, 4), (1, 4)) - .Transform(Matrix3x2.CreateScale(200F)); + .Transform(Matrix4x4.CreateScale(200F)); Rectangle interest = Rectangle.Ceiling(path.Bounds); RasterizerOptions options = new(interest, rule, RasterizationMode.Antialiased, RasterizerSamplingOrigin.PixelBoundary, 0.5f); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs index 1968ec8af..a292c0d08 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs @@ -164,7 +164,7 @@ public void DefaultTransform() Vector2 point1 = new(10, 10); Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(new Vector2(5, 5)); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(5, 5, 0); PathBuilder builder = new(matrix); builder.AddLines(point1, point2, point3); @@ -178,7 +178,7 @@ public void SetTransform() Vector2 point1 = new(10, 10); Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(new Vector2(100, 100)); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(100, 100, 0); PathBuilder builder = new(); builder.AddLines(point1, point2, point3); @@ -202,7 +202,7 @@ public void SetOriginLeaveMatrix() Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); Vector2 origin = new(-50, -100); - PathBuilder builder = new(Matrix3x2.CreateScale(10)); + PathBuilder builder = new(Matrix4x4.CreateScale(10)); builder.AddLines(point1, point2, point3); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs index 77314de9e..688a0a3e7 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs @@ -23,18 +23,18 @@ public void RotateInRadians() { const float Angle = (float)Math.PI; - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateRotation(Angle, RectangleF.Center(this.bounds)); + Matrix4x4 targetMatrix = new(Matrix3x2.CreateRotation(Angle, RectangleF.Center(this.bounds))); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Rotate(Angle); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -42,20 +42,20 @@ public void RotateInDegrees() { const float Angle = 90; - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here const float Radians = (float)(Math.PI * Angle / 180.0); - Matrix3x2 targetMatrix = Matrix3x2.CreateRotation(Radians, RectangleF.Center(this.bounds)); + Matrix4x4 targetMatrix = new(Matrix3x2.CreateRotation(Radians, RectangleF.Center(this.bounds))); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.RotateDegree(Angle); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -63,18 +63,18 @@ public void TranslateVector() { Vector2 point = new(98, 120); - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateTranslation(point); + Matrix4x4 targetMatrix = Matrix4x4.CreateTranslation(point.X, point.Y, 0); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Translate(point); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -83,17 +83,17 @@ public void TranslateXY() const float X = 76; const float Y = 7; - this.mockPath.Setup(p => p.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(p => p.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateTranslation(new Vector2(X, Y)); + Matrix4x4 targetMatrix = Matrix4x4.CreateTranslation(X, Y, 0); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Translate(X, Y); - this.mockPath.Verify(p => p.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(p => p.Transform(It.IsAny()), Times.Once); } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs index f5d9ca383..aa6406b26 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs @@ -145,7 +145,7 @@ public void ShapePaths() public void TransformIdentityReturnsShapeObject() { IPath shape = new RectangularPolygon(0, 0, 200, 60); - IPath transformedShape = shape.Transform(Matrix3x2.Identity); + IPath transformedShape = shape.Transform(Matrix4x4.Identity); Assert.Same(shape, transformedShape); } @@ -155,7 +155,7 @@ public void Transform() { IPath shape = new RectangularPolygon(0, 0, 200, 60); - IPath newShape = shape.Transform(new Matrix3x2(0, 1, 1, 0, 20, 2)); + IPath newShape = shape.Transform(new Matrix4x4(new Matrix3x2(0, 1, 1, 0, 20, 2))); Assert.Equal(new PointF(20, 2), newShape.Bounds.Location); Assert.Equal(new SizeF(60, 200), newShape.Bounds.Size); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs b/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs index a9f82b028..7eb1cfb80 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs @@ -20,7 +20,7 @@ public static IPath IrisSegment(int rotationPos) new Vector2(78.26f, 97.0461f))).Translate(center - segmentRotationCenter); float angle = rotationPos * ((float)Math.PI / 3); - return segment.Transform(Matrix3x2.CreateRotation(angle, center)); + return segment.Transform(new Matrix4x4(Matrix3x2.CreateRotation(angle, center))); } public static IPath IrisSegment(float size, int rotationPos) @@ -39,9 +39,9 @@ public static IPath IrisSegment(float size, int rotationPos) float angle = rotationPos * ((float)Math.PI / 3); - IPath rotated = segment.Transform(Matrix3x2.CreateRotation(angle, center)); + IPath rotated = segment.Transform(new Matrix4x4(Matrix3x2.CreateRotation(angle, center))); - Matrix3x2 scaler = Matrix3x2.CreateScale(scalingFactor, Vector2.Zero); + Matrix4x4 scaler = Matrix4x4.CreateScale(scalingFactor); IPath scaled = rotated.Transform(scaler); return scaled; diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs index 6b027a7a0..2e0de6699 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs @@ -27,14 +27,14 @@ public void Polygon(IPath path, float gridSize = 10f, float scale = 10f, [Caller return; } - path = path.Transform(Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(gridSize, gridSize)); + path = path.Transform(Matrix4x4.CreateScale(scale) * Matrix4x4.CreateTranslation(gridSize, gridSize, 0)); RectangleF bounds = path.Bounds; gridSize *= scale; using Image img = new Image((int)(bounds.Right + (2 * gridSize)), (int)(bounds.Bottom + (2 * gridSize))); img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => { - canvas.Fill(path, TestBrush); + canvas.Fill(TestBrush, path); DrawGrid(canvas, bounds, gridSize); })); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs index e0bdfa02f..5d4920e61 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs @@ -19,7 +19,7 @@ internal static class PolygonFactory // based on: // https://github.com/SixLabors/ImageSharp.Drawing/issues/15#issuecomment-521061283 - public static IReadOnlyList GetGeoJsonPoints(Feature geometryOwner, Matrix3x2 transform) + public static IReadOnlyList GetGeoJsonPoints(Feature geometryOwner, Matrix4x4 transform) { List result = []; IGeometryObject geometry = geometryOwner.Geometry; @@ -64,14 +64,14 @@ PointF PositionToPointF(IPosition pos) } } - public static PointF[][] GetGeoJsonPoints(string geoJsonContent, Matrix3x2 transform) + public static PointF[][] GetGeoJsonPoints(string geoJsonContent, Matrix4x4 transform) { FeatureCollection features = JsonConvert.DeserializeObject(geoJsonContent); return features.Features.SelectMany(f => GetGeoJsonPoints(f, transform)).ToArray(); } public static PointF[][] GetGeoJsonPoints(string geoJsonContent) => - GetGeoJsonPoints(geoJsonContent, Matrix3x2.Identity); + GetGeoJsonPoints(geoJsonContent, Matrix4x4.Identity); public static Polygon CreatePolygon(params (float X, float Y)[] coords) => new(new LinearLineSegment(CreatePointArray(coords))) diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png index 57d0f71c6..43ad24b93 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75b97ff54f46fe7eaa83b2ba7d10d0194a9b9c7f92ccc0d1a5f585befccdfad1 -size 683 +oid sha256:38a7090de2e49df30cbfa51953f77d5e0584438602265ff6a48ad75c53e4e8e6 +size 674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png index 8bb66b90a..c0decb5c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33161f003216fe713bf75717480719d1f1dafde24bfbd8fc31943c4861de3e0e -size 1658 +oid sha256:0890bc4fde8013d814458a2ec34ed1e51c232e1e99c31b84dc7ae023c8bf62a1 +size 1548 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png index b47df4405..5b210ff3a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:239197be7095b593ed26dac74b25a3c898525380b3bad867d5ea02dd9bbfb9d8 -size 1416 +oid sha256:d6412666ed00fd3b2dcf6ce7645dec3fdfa5fd029651f8166b9e7f34bf5b47d4 +size 1704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png index 1941534cc..e6f419818 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 -size 1544 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png index 494270cdd..75186d636 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaeb64517972ae4779e416495995f1786d09172adce7e3f0ea54b7865d7ea4b4 -size 1751 +oid sha256:ba202c83d3bf4a900af8654ea8b231393a4182d7befbbf895fff418e37bcadf1 +size 1996 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png index a0734ee4e..0db8ffed5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99e64479ca8316fda5b758b8a0a7e25f67bddc8704e525af8085b843e693b7c9 -size 1998 +oid sha256:804e9a7d31d80705e5005a39edbe28dd8ae766b2885e38e45933a62de2b9509e +size 2114 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png index 4ea595dff..4183ebed9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d60ad285866d2042734cae2a0ce2d47035ff74d545604ff2491140cfc55cc59 -size 2275 +oid sha256:ffc51d54e067ac514425d9ea9b19f2fc5f0b4af11451be153c1da29885ede640 +size 2225 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png index 57d0f71c6..43ad24b93 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75b97ff54f46fe7eaa83b2ba7d10d0194a9b9c7f92ccc0d1a5f585befccdfad1 -size 683 +oid sha256:38a7090de2e49df30cbfa51953f77d5e0584438602265ff6a48ad75c53e4e8e6 +size 674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png index 50fc63296..06c04c71c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:428bb82e650ec3bc35540fbb03d3da3ee976e18bb2bcee8a022ce9de9be3ea0c -size 965 +oid sha256:6e2dc06fdec0dd48bd1e4e29420bf25e094fcf98263634e8efb0b11f85f5892b +size 932 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png index bb7c6e05b..90d85f0fb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de3d0a7937b6afb5996a00df048e3cf0799e09a7aeda53dadaa14f534d486aed -size 812 +oid sha256:a742a4e1ff6786d4435a3abfae5f3de97b8409434a7fc7c7d8c567cdf523c7b8 +size 796 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png index d6383b786..27a056b60 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f5baea6da8377f4a01bc57de4619c737a9650b7907076dfa9ebd0bc8ca0a97f -size 542 +oid sha256:0dbe6cef98d0bd165a63aeb282e21ac1ff480171af32d9d2babfe75113a5da57 +size 535 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png index 8bb66b90a..c0decb5c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33161f003216fe713bf75717480719d1f1dafde24bfbd8fc31943c4861de3e0e -size 1658 +oid sha256:0890bc4fde8013d814458a2ec34ed1e51c232e1e99c31b84dc7ae023c8bf62a1 +size 1548 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png index cdfea92db..82fe59ac1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a0684defec9733bc839cd55ebfd446782a714a643acddabc331b2d0e7382b86 -size 1580 +oid sha256:97cf0529ee30218df153d180864938918f9ef118cdae35f6d0d347e088b41776 +size 1445 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png index 9c9a12a7f..a3f902e34 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7a41dd06dbdcb2c519640df53d268d6c6c4498484e0b5eecb28b7a18d8394a1 -size 1412 +oid sha256:6aa7cda377b908fb4c4cd9ae1b8e2dfc493d82f6771a1ad3e074657c8c431c0f +size 1329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png index 20b2f2c5c..d8aee09ab 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:824f947ccfadc702799556afe6b2c05ef5bddf453d0522b301fb837d255ff0ad -size 1558 +oid sha256:38c1d989c31baee7445c099b643aec6a7accef8bff7455bd5f44c86133fbf580 +size 1451 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png index b47df4405..5b210ff3a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:239197be7095b593ed26dac74b25a3c898525380b3bad867d5ea02dd9bbfb9d8 -size 1416 +oid sha256:d6412666ed00fd3b2dcf6ce7645dec3fdfa5fd029651f8166b9e7f34bf5b47d4 +size 1704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png index 21eeb3737..d96dd5716 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ce285d7df77e1ce59de7590d993b1f29b5822504ec1caa2a30a4af080a6c522 -size 1683 +oid sha256:dd10694b16b98783c3aa16ac9e50501fff4c5c9a5dd9a78ee91dd2af8c949f8c +size 1687 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png index 62a24e749..322d69db7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f793714c9049cb99946a2e0fb0581bff65ba62533ef75b99baf34d75131670a -size 1760 +oid sha256:405686e70c0f341f219496dd0b34e7ad5373ac43eb38358dee9a5e59d67cc77a +size 1674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png index 996b694a2..f24da8215 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec85374ced15e9af4fe6598c1a4f380a8c2726b3f1d65981ee3047ac6518f87 -size 1310 +oid sha256:3d75fa6b243b87202ec4d7a599378c9b4195c15c772a36c7aeafb64e41263ce3 +size 1597 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png index 1941534cc..e6f419818 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 -size 1544 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png index 3dcb40ed3..3e9e0fd81 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2424ada6ae6672f808b666d0b603cebc912a17f620ac86d88ae94b8e73839ad9 -size 1482 +oid sha256:a7cdc8e25d6545f4d5ce0265dd152e58748f7740d071e257af671abca5f36bd7 +size 1801 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png index fa021944a..f1b795aab 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa55f990bca716f98eee8675fca9536c821dcd1944b229d469f93104ebca4a2f -size 1589 +oid sha256:9ae44f19a7e2c1aba894bf706a33b2fd533e1718a5c895132007db06eee5ffdc +size 1861 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png index 1941534cc..e6f419818 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18d9229d8810819273fc2f2e4fa9c1a28d8144ab8694356f924bae65c6c6b6d5 -size 1544 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png new file mode 100644 index 000000000..b6b52d7ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a862400d229f6cbbb8d609490bd82c5403c0e0eed9e366ef0b3c7df3bca8da9 +size 31777 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png new file mode 100644 index 000000000..0ab0b108f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b71e2d978dd559a71c9559fbb77ac756faeb3e73c6e6beef8b35c29eaed13f23 +size 31691 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0ab0b108f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b71e2d978dd559a71c9559fbb77ac756faeb3e73c6e6beef8b35c29eaed13f23 +size 31691 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png index dd7273ec4..0312b3f84 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb -size 4892 +oid sha256:37df4c3c1e2174e4ce5ccc1cf56463e0232e8147106e996c432233eb56f31b48 +size 4349 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png index dd7273ec4..0312b3f84 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb -size 4892 +oid sha256:37df4c3c1e2174e4ce5ccc1cf56463e0232e8147106e996c432233eb56f31b48 +size 4349 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png index dd7273ec4..0312b3f84 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44edb7169081e2e33df8e65c4d37d2cfa5aba385a5491157b614c144becb14cb -size 4892 +oid sha256:37df4c3c1e2174e4ce5ccc1cf56463e0232e8147106e996c432233eb56f31b48 +size 4349 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png index e2da3554e..068a89979 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16243d82d61d8ce66a9a8ce3584d9f22ec990aa79e31bd2aecc0de065efa2bae -size 99045 +oid sha256:13405f754723b9cff04307e94079a2826a25c20e16957bb5abea556aceea4399 +size 38897 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png index 4a6e9867b..8bf73279f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ecf0746020a0f89895a19f36135e48bbe7fdaf5069123da0b12942881d7cb77 -size 99042 +oid sha256:ca53253afc2f40e32a424bf6deb0791f3d688baacaf8c86662cea97dd5fd0dea +size 38880 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png index 4a6e9867b..8bf73279f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ecf0746020a0f89895a19f36135e48bbe7fdaf5069123da0b12942881d7cb77 -size 99042 +oid sha256:ca53253afc2f40e32a424bf6deb0791f3d688baacaf8c86662cea97dd5fd0dea +size 38880 From 2115d36e3fa27ca2b3278740bd345aa6bf64c171 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:12:07 +1000 Subject: [PATCH 116/136] Attempt to run WebGPU tests in CI --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 234e7e7c8..5e08199c7 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#if !ENV_CI +#if !ENV_CI_BAK // WebGPU is failing in our CI environment in Ubuntu with // WebGPU adapter request failed with status 'Unavailable' // It's also failing in Windows CI with "Test host process crashed : Fatal error.0xC0000005" From 4f3775902cb88bd9100a4f317c3365fef011e2f4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:19:28 +1000 Subject: [PATCH 117/136] Use explicit theory with runtime support check --- .../WebGPUDrawingBackend.cs | 18 +++++ .../Backends/WebGPUDrawingBackendTests.cs | 71 +++++++++---------- .../Attributes/WebGPUAttributes.cs | 34 +++++++++ 3 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 2bd9aabb7..df50b1bd1 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -75,6 +75,24 @@ private enum PreparedBrushType : uint Recolor = 8, } + /// + /// Gets a value indicating whether WebGPU is available on the current system. + /// This probes the runtime by attempting to acquire an adapter and device. + /// + /// when WebGPU is functional; otherwise . + public static bool IsSupported() + { + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); + } + catch + { + return false; + } + } + /// /// Gets the testing-only diagnostic counter for total coverage preparation requests. /// diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 5e08199c7..4277ea3f7 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -1,15 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#if !ENV_CI_BAK -// WebGPU is failing in our CI environment in Ubuntu with -// WebGPU adapter request failed with status 'Unavailable' -// It's also failing in Windows CI with "Test host process crashed : Fatal error.0xC0000005" -// TODO: Ask the Silk.NET team for help. using System.Numerics; using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -35,7 +31,7 @@ public class WebGPUDrawingBackendTests { PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Clear } }; - [Theory] + [WebGPUTheory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -89,7 +85,7 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -129,7 +125,7 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } - [Theory] + [WebGPUTheory] [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -190,7 +186,7 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -270,7 +266,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } - [Theory] + [WebGPUTheory] [WithBasicTestPatternImages(nameof(GraphicsOptionsModePairs), 384, 256, PixelTypes.Rgba32)] public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput( TestImageProvider provider, @@ -325,7 +321,7 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput( TestImageProvider provider, @@ -381,7 +377,7 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -443,7 +439,7 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.009F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -487,7 +483,7 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -533,7 +529,7 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } - [Theory] + [WebGPUTheory] [WithBlankImage(220, 160, PixelTypes.Rgba32)] public void Process_WithWebGPUBackend_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -573,7 +569,7 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.0516F); } - [Theory] + [WebGPUTheory] [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -630,7 +626,7 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi AssertGpuPathWhenRequired(nativeSurfaceBackend); } - [Theory] + [WebGPUTheory] [WithBlankImage(1200, 280, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1032,7 +1028,7 @@ private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) backend.TestingFallbackCompositeCoverageCallCount); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32)] public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1084,7 +1080,7 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider(TestImageProvi AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Butt)] [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Square)] [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Round)] @@ -1188,7 +1184,7 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] public void FillPath_MultipleSeparatePaths_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1236,7 +1232,7 @@ void DrawAction(DrawingCanvas canvas) AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1305,7 +1301,7 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(800, 600, "White", PixelTypes.Rgba32)] public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1345,7 +1341,7 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(300, 200, "White", PixelTypes.Rgba32)] public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1460,7 +1456,7 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag } } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1506,7 +1502,7 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1551,7 +1547,7 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1596,7 +1592,7 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1643,7 +1639,7 @@ public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1689,7 +1685,7 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(Test AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1737,7 +1733,7 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1783,7 +1779,7 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1823,7 +1819,7 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProv AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1863,7 +1859,7 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(Test AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "Red", PixelTypes.Rgba32)] public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1903,7 +1899,7 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); } - [Theory] + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1949,7 +1945,7 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -1996,7 +1992,7 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -2234,4 +2230,3 @@ private static Buffer2DRegion GetFrameRegion(Image image where TPixel : unmanaged, IPixel => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); } -#endif diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs new file mode 100644 index 000000000..aeb63f44f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; + +/// +/// A that skips when WebGPU is not available on the current system. +/// +public class WebGPUFactAttribute : FactAttribute +{ + public WebGPUFactAttribute() + { + if (!WebGPUDrawingBackend.IsSupported()) + { + this.Skip = "WebGPU is not available on this system."; + } + } +} + +/// +/// A that skips when WebGPU is not available on the current system. +/// +public class WebGPUTheoryAttribute : TheoryAttribute +{ + public WebGPUTheoryAttribute() + { + if (!WebGPUDrawingBackend.IsSupported()) + { + this.Skip = "WebGPU is not available on this system."; + } + } +} From d88e6de8a3883a8d774c35a30e92f960e65d0d60 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:20:41 +1000 Subject: [PATCH 118/136] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5824fae3b..c2af18d00 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -92,12 +92,12 @@ jobs: framework: net8.0 sdk: 8.0.x runtime: -x64 - codecov: true + codecov: false - os: macos-26 framework: net8.0 sdk: 8.0.x runtime: -x64 - codecov: false + codecov: true - os: windows-latest framework: net8.0 sdk: 8.0.x From e1c2605fecf8adec6203292d534a7156097997f2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:34:28 +1000 Subject: [PATCH 119/136] Cache WebGPU support and gate drawing backend --- .../WebGPUDrawingBackend.cs | 41 ++++++++++--------- .../Processing/Backends/IDrawingBackend.cs | 5 +++ .../RasterizerDefaultsExtensions.cs | 6 ++- ...PUAttributes.cs => WebGPUFactAttribute.cs} | 6 ++- 4 files changed, 35 insertions(+), 23 deletions(-) rename tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/{WebGPUAttributes.cs => WebGPUFactAttribute.cs} (82%) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index df50b1bd1..49ab99749 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -49,6 +49,7 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; + private static bool? isSupported; private bool isDisposed; private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); @@ -75,24 +76,6 @@ private enum PreparedBrushType : uint Recolor = 8, } - /// - /// Gets a value indicating whether WebGPU is available on the current system. - /// This probes the runtime by attempting to acquire an adapter and device. - /// - /// when WebGPU is functional; otherwise . - public static bool IsSupported() - { - try - { - using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); - return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); - } - catch - { - return false; - } - } - /// /// Gets the testing-only diagnostic counter for total coverage preparation requests. /// @@ -154,6 +137,26 @@ public static bool IsSupported() /// internal int TestingComputePathBatchCount { get; private set; } + /// + /// Gets a value indicating whether WebGPU is available on the current system. + /// This probes the runtime by attempting to acquire an adapter and device. + /// The result is cached after the first probe. + /// + public bool IsSupported => isSupported ??= ProbeSupport(); + + private static bool ProbeSupport() + { + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); + } + catch + { + return false; + } + } + /// public void FlushCompositions( Configuration configuration, @@ -324,7 +327,7 @@ public void ReleaseFrameResources( ICanvasFrame target) where TPixel : unmanaged, IPixel { - nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(target); + nint targetIdentity = RuntimeHelpers.GetHashCode(target); WebGPUFlushContext.ReleaseCpuTargetEntries(targetIdentity); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index e9cfe7a28..53b2b0f15 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// public interface IDrawingBackend { + /// + /// Gets a value indicating whether this backend is available on the current system. + /// + public bool IsSupported => true; + /// /// Flushes queued composition operations for the target. /// diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs index f662091d1..c600a40de 100644 --- a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -43,7 +43,8 @@ public static void SetDrawingBackend(this Configuration configuration, IDrawingB internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext context) { if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is IDrawingBackend configured) + backend is IDrawingBackend configured && + configured.IsSupported) { return configured; } @@ -59,7 +60,8 @@ internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext c internal static IDrawingBackend GetDrawingBackend(this Configuration configuration) { if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is IDrawingBackend configured) + backend is IDrawingBackend configured && + configured.IsSupported) { return configured; } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs similarity index 82% rename from tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs rename to tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs index aeb63f44f..3ad17bdec 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUAttributes.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs @@ -12,7 +12,8 @@ public class WebGPUFactAttribute : FactAttribute { public WebGPUFactAttribute() { - if (!WebGPUDrawingBackend.IsSupported()) + using WebGPUDrawingBackend backend = new(); + if (!backend.IsSupported) { this.Skip = "WebGPU is not available on this system."; } @@ -26,7 +27,8 @@ public class WebGPUTheoryAttribute : TheoryAttribute { public WebGPUTheoryAttribute() { - if (!WebGPUDrawingBackend.IsSupported()) + using WebGPUDrawingBackend backend = new(); + if (!backend.IsSupported) { this.Skip = "WebGPU is not available on this system."; } From e058bf3f8770b65a76dab63273955f361f6614af Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 13:46:56 +1000 Subject: [PATCH 120/136] Use PNG for comparison --- .../ProcessWithDrawingCanvasTests.Polygons.cs | 19 ++++++++----------- .../FillPolygon_IntersectionRules_Nonzero.png | 3 +++ ...onzero_Rgba32_Solid60x60_(0,0,255,255).bmp | 3 --- .../FillPolygon_IntersectionRules_OddEven.png | 3 +++ ...ddEven_Rgba32_Solid60x60_(0,0,255,255).bmp | 3 --- 5 files changed, 14 insertions(+), 17 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp create mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs index fde0f27f1..b4cc8cdee 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs @@ -366,7 +366,6 @@ public void FillPolygon_EllipsePolygon(TestImageProvider provide public void FillPolygon_IntersectionRules_OddEven(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image img = provider.GetImage(); Polygon poly = new(new LinearLineSegment( new PointF(10, 30), new PointF(10, 20), @@ -385,10 +384,10 @@ public void FillPolygon_IntersectionRules_OddEven(TestImageProvider c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly))); - - provider.Utility.SaveTestOutputFile(img); - Assert.Equal(Color.Blue.ToPixel(), img[25, 25]); + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly)), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] @@ -396,8 +395,6 @@ public void FillPolygon_IntersectionRules_OddEven(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - Configuration.Default.MaxDegreeOfParallelism = 1; - using Image img = provider.GetImage(); Polygon poly = new(new LinearLineSegment( new PointF(10, 30), new PointF(10, 20), @@ -416,9 +413,9 @@ public void FillPolygon_IntersectionRules_Nonzero(TestImageProvider c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly))); - - provider.Utility.SaveTestOutputFile(img); - Assert.Equal(Color.HotPink.ToPixel(), img[25, 25]); + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly)), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } } diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png new file mode 100644 index 000000000..92928f27e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce3f3b0f2bf919cf64fd1b01b0be62df81a0ba8221124ee37347a786a60dbd2c +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp deleted file mode 100644 index e191ef63d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba74a337d12292afa1be49e6dd7684c1b2302142c3e6f127e6d0a9ca68490a29 -size 14454 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png new file mode 100644 index 000000000..d2bec2891 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7ede75d604ad556527a04488a06085cdba2c4a044a60844a02dcf0c07a6eb58 +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp deleted file mode 100644 index 4c06a02b9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6f9b227887ea996980662411698876098dee5c5533713b4e499835437f0cd86 -size 14454 From 76a17bc8e6046b57896c71c3f9dbb88e71b6275e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 14:01:55 +1000 Subject: [PATCH 121/136] Update WebGPUDrawingBackendTests.cs --- .../Backends/WebGPUDrawingBackendTests.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 4277ea3f7..974de1c81 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -891,7 +891,8 @@ private static void DebugSaveBackendTriplet( string testName, Image defaultImage, Image cpuRegionImage, - Image nativeSurfaceImage) + Image nativeSurfaceImage, + float tolerantPercentage = 0.0003F) where TPixel : unmanaged, IPixel { defaultImage.DebugSave( @@ -912,7 +913,7 @@ private static void DebugSaveBackendTriplet( appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(0.0003F); + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(tolerantPercentage); defaultImage.CompareToReferenceOutput( tolerantComparer, provider, @@ -1494,7 +1495,8 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_LinearGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); + // MacOS on CI has some outliers with this test, so using a slightly higher tolerance here to avoid noise. + DebugSaveBackendTriplet(provider, "FillPath_LinearGradient", defaultImage, cpuRegionImage, nativeSurfaceImage, tolerantPercentage: 0.0007F); AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(cpuRegionBackend); @@ -1771,7 +1773,15 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput Date: Tue, 10 Mar 2026 14:07:54 +1000 Subject: [PATCH 122/136] Remove duplicate assertion --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 974de1c81..d8c4ee9d3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -1786,7 +1786,6 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput Date: Tue, 10 Mar 2026 14:18:55 +1000 Subject: [PATCH 123/136] Verify WebGPU compute pipeline in ProbeSupport --- .../WebGPUDrawingBackend.cs | 94 ++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 49ab99749..1c7d3fb4d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -144,12 +144,102 @@ private enum PreparedBrushType : uint /// public bool IsSupported => isSupported ??= ProbeSupport(); - private static bool ProbeSupport() + /// + /// Determines whether WebGPU compute support is available on the current system. + /// + /// + /// This method goes beyond checking adapter and device availability — it also compiles + /// a trivial compute shader and creates a compute pipeline to verify the full compute + /// path works. Some systems report a valid device but crash on pipeline creation due to + /// driver or runtime issues. + /// + /// Returns if WebGPU compute support is available; otherwise, . + public static bool ProbeSupport() { try { using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); - return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out _, out _)) + { + return false; + } + + WebGPU api = lease.Api; + + // Compile a trivial compute shader and create a pipeline to verify the + // full compute path works end-to-end. Some drivers/runtimes crash at + // DeviceCreateComputePipeline despite successful device creation. + ReadOnlySpan probeShader = "@compute @workgroup_size(1) fn cs_main() {}\0"u8; + fixed (byte* shaderCodePtr = probeShader) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + ShaderModule* shaderModule = api.DeviceCreateShaderModule(device, in shaderDescriptor); + if (shaderModule is null) + { + return false; + } + + try + { + ReadOnlySpan entryPoint = "cs_main\0"u8; + fixed (byte* entryPointPtr = entryPoint) + { + ProgrammableStageDescriptor computeStage = new() + { + Module = shaderModule, + EntryPoint = entryPointPtr + }; + + PipelineLayoutDescriptor layoutDescriptor = new() + { + BindGroupLayoutCount = 0, + BindGroupLayouts = null + }; + + PipelineLayout* pipelineLayout = api.DeviceCreatePipelineLayout(device, in layoutDescriptor); + if (pipelineLayout is null) + { + return false; + } + + try + { + ComputePipelineDescriptor pipelineDescriptor = new() + { + Layout = pipelineLayout, + Compute = computeStage + }; + + ComputePipeline* pipeline = api.DeviceCreateComputePipeline(device, in pipelineDescriptor); + if (pipeline is null) + { + return false; + } + + api.ComputePipelineRelease(pipeline); + return true; + } + finally + { + api.PipelineLayoutRelease(pipelineLayout); + } + } + } + finally + { + api.ShaderModuleRelease(shaderModule); + } + } } catch { From 08f2985ab2a93d05300cdabb5ac6ab8f9ea58d7a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 15:01:07 +1000 Subject: [PATCH 124/136] Add RemoteExecutor and WebGPU probe --- .../ImageSharp.Drawing.WebGPU.csproj | 7 + .../RemoteExecutor/Program.cs | 29 ++++ .../RemoteExecutor/RemoteExecutor.cs | 151 ++++++++++++++++++ .../WebGPUDrawingBackend.cs | 77 +++++++-- .../Attributes/WebGPUFactAttribute.cs | 40 ++++- 5 files changed, 279 insertions(+), 25 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index 8e6c3b941..657b3660b 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -12,6 +12,13 @@ Image Draw Shape Path Font An extension to ImageSharp that allows the drawing of images, paths, and text. Debug;Release + + Exe true true diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs new file mode 100644 index 000000000..d91a0b881 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Entry point for child processes spawned by . +/// Dispatches to the requested probe method by name. +/// Adapted from Microsoft.DotNet.RemoteExecutor (MIT license). +/// +internal static class Program +{ + private static int Main(string[] args) + { + if (args.Length < 1) + { + Console.Error.WriteLine("Usage: {0} methodName", typeof(Program).Assembly.GetName().Name); + return -1; + } + + string methodName = args[0]; + + return methodName switch + { + nameof(WebGPUDrawingBackend.ProbeComputePipelineSupport) => WebGPUDrawingBackend.ProbeComputePipelineSupport(), + _ => -1 + }; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs new file mode 100644 index 000000000..619d58f98 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using IOPath = System.IO.Path; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Minimal remote executor that invokes a named method in a child process. +/// The child process entry point () dispatches to +/// the requested method by name — no reflection is used. +/// Adapted from Microsoft.DotNet.RemoteExecutor (MIT license). +/// +internal static class RemoteExecutor +{ + private static readonly string? AssemblyPath; + private static readonly string? HostRunner; + private static readonly string? RuntimeConfigPath; + private static readonly string? DepsJsonPath; + + static RemoteExecutor() + { + if (!IsSupported) + { + return; + } + + string? processFileName = Process.GetCurrentProcess().MainModule?.FileName; + if (processFileName is null) + { + return; + } + + string baseDir = AppContext.BaseDirectory; + string assemblyName = typeof(RemoteExecutor).Assembly.GetName().Name!; + AssemblyPath = IOPath.Combine(baseDir, assemblyName + ".dll"); + if (!File.Exists(AssemblyPath)) + { + return; + } + + HostRunner = processFileName; + string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + + if (!IOPath.GetFileName(HostRunner).Equals(hostName, StringComparison.OrdinalIgnoreCase)) + { + string runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); + string? directory = IOPath.GetDirectoryName(IOPath.GetDirectoryName(IOPath.GetDirectoryName(runtimeDir))); + if (directory is not null) + { + string dotnetExe = IOPath.Combine(directory, hostName); + if (File.Exists(dotnetExe)) + { + HostRunner = dotnetExe; + } + } + } + + string runtimeConfigCandidate = IOPath.Combine(baseDir, assemblyName + ".runtimeconfig.json"); + string depsJsonCandidate = IOPath.Combine(baseDir, assemblyName + ".deps.json"); + + RuntimeConfigPath = File.Exists(runtimeConfigCandidate) ? runtimeConfigCandidate : null; + DepsJsonPath = File.Exists(depsJsonCandidate) ? depsJsonCandidate : null; + } + + /// + /// Gets a value indicating whether this remote executor is supported on the current platform. + /// + internal static bool IsSupported { get; } = + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("WASI")) && + Environment.GetEnvironmentVariable("DOTNET_REMOTEEXECUTOR_SUPPORTED") != "0"; + + /// + /// Invokes the specified static method in a child process and returns its exit code. + /// The method name is dispatched by via a switch statement, + /// so no reflection is needed in the child process. + /// + /// A static method returning (the exit code). + /// Maximum time to wait for the child process. + /// The exit code from the child process, or -1 on failure. + internal static int Invoke(Func method, int timeoutMilliseconds = 30_000) + { + if (!IsSupported || AssemblyPath is null || HostRunner is null) + { + return -1; + } + + string methodName = method.Method.Name; + + string args = "exec"; + if (RuntimeConfigPath is not null) + { + args += $" --runtimeconfig \"{RuntimeConfigPath}\""; + } + + if (DepsJsonPath is not null) + { + args += $" --depsfile \"{DepsJsonPath}\""; + } + + args += $" \"{AssemblyPath}\" \"{methodName}\""; + + ProcessStartInfo psi = new() + { + FileName = HostRunner, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true + }; + + // Remove profiler environment variables from child process. + psi.Environment.Remove("Cor_Profiler"); + psi.Environment.Remove("Cor_Enable_Profiling"); + psi.Environment.Remove("CoreClr_Profiler"); + psi.Environment.Remove("CoreClr_Enable_Profiling"); + + try + { + using Process? process = Process.Start(psi); + if (process is null) + { + return -1; + } + + if (!process.WaitForExit(timeoutMilliseconds)) + { + try + { + process.Kill(); + } + catch + { + // Ignore cleanup errors. + } + + return -1; + } + + return process.ExitCode; + } + catch + { + return -1; + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 1c7d3fb4d..4e1dd1fd8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -142,33 +142,76 @@ private enum PreparedBrushType : uint /// This probes the runtime by attempting to acquire an adapter and device. /// The result is cached after the first probe. /// - public bool IsSupported => isSupported ??= ProbeSupport(); + public bool IsSupported => isSupported ??= ProbeFullSupport(); /// - /// Determines whether WebGPU compute support is available on the current system. + /// Probes whether WebGPU compute is fully supported on the current system. + /// First checks adapter/device availability in-process. If that succeeds, + /// spawns a child process via to test compute + /// pipeline creation, which can crash with an unrecoverable access violation + /// on some systems. If the remote executor is not available, falls back to + /// the device-only check. /// - /// - /// This method goes beyond checking adapter and device availability — it also compiles - /// a trivial compute shader and creates a compute pipeline to verify the full compute - /// path works. Some systems report a valid device but crash on pipeline creation due to - /// driver or runtime issues. - /// /// Returns if WebGPU compute support is available; otherwise, . + private static bool ProbeFullSupport() + { + // Step 1: Quick in-process check for adapter/device availability. + if (!ProbeSupport()) + { + return false; + } + + // Step 2: Out-of-process probe for compute pipeline support. + // DeviceCreateComputePipeline can crash with an AccessViolationException + // on some systems (e.g. Windows CI with software renderers). This native + // crash cannot be caught in managed code, so we run it in a child process. + if (!RemoteExecutor.IsSupported) + { + // If we can't spawn a child process, assume device availability is sufficient. + return true; + } + + return RemoteExecutor.Invoke(ProbeComputePipelineSupport) == 0; + } + + /// + /// Determines whether WebGPU adapter and device are available on the current system. + /// + /// This method only checks adapter and device availability. It does not attempt + /// compute pipeline creation. Use for a complete check. + /// Returns if a WebGPU device is available; otherwise, . public static bool ProbeSupport() + { + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); + } + catch + { + return false; + } + } + + /// + /// Probes full WebGPU compute pipeline support by compiling a trivial shader and + /// creating a compute pipeline. This method may crash with an access violation on + /// systems with broken WebGPU compute support — callers should run it in a child + /// process (e.g. via RemoteExecutor) to isolate the crash. + /// + /// Exit code: 0 on success, 1 on failure. + public static int ProbeComputePipelineSupport() { try { using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out _, out _)) { - return false; + return 1; } WebGPU api = lease.Api; - // Compile a trivial compute shader and create a pipeline to verify the - // full compute path works end-to-end. Some drivers/runtimes crash at - // DeviceCreateComputePipeline despite successful device creation. ReadOnlySpan probeShader = "@compute @workgroup_size(1) fn cs_main() {}\0"u8; fixed (byte* shaderCodePtr = probeShader) { @@ -186,7 +229,7 @@ public static bool ProbeSupport() ShaderModule* shaderModule = api.DeviceCreateShaderModule(device, in shaderDescriptor); if (shaderModule is null) { - return false; + return 1; } try @@ -209,7 +252,7 @@ public static bool ProbeSupport() PipelineLayout* pipelineLayout = api.DeviceCreatePipelineLayout(device, in layoutDescriptor); if (pipelineLayout is null) { - return false; + return 1; } try @@ -223,11 +266,11 @@ public static bool ProbeSupport() ComputePipeline* pipeline = api.DeviceCreateComputePipeline(device, in pipelineDescriptor); if (pipeline is null) { - return false; + return 1; } api.ComputePipelineRelease(pipeline); - return true; + return 0; } finally { @@ -243,7 +286,7 @@ public static bool ProbeSupport() } catch { - return false; + return 1; } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs index 3ad17bdec..18eac23f1 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs @@ -6,31 +6,55 @@ namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; /// -/// A that skips when WebGPU is not available on the current system. +/// A that skips when WebGPU compute is not available on the current system. /// public class WebGPUFactAttribute : FactAttribute { public WebGPUFactAttribute() { - using WebGPUDrawingBackend backend = new(); - if (!backend.IsSupported) + if (!WebGPUProbe.IsComputeSupported) { - this.Skip = "WebGPU is not available on this system."; + this.Skip = "WebGPU compute is not available on this system."; } } } /// -/// A that skips when WebGPU is not available on the current system. +/// A that skips when WebGPU compute is not available on the current system. /// public class WebGPUTheoryAttribute : TheoryAttribute { public WebGPUTheoryAttribute() { - using WebGPUDrawingBackend backend = new(); - if (!backend.IsSupported) + if (!WebGPUProbe.IsComputeSupported) { - this.Skip = "WebGPU is not available on this system."; + this.Skip = "WebGPU compute is not available on this system."; + } + } +} + +/// +/// Caches the result of the WebGPU compute pipeline probe. +/// The backend's already performs +/// a full out-of-process probe via the internal RemoteExecutor, so we simply +/// instantiate the backend and check its result. +/// +internal static class WebGPUProbe +{ + private static bool? computeSupported; + + internal static bool IsComputeSupported => computeSupported ??= Probe(); + + private static bool Probe() + { + try + { + using WebGPUDrawingBackend backend = new(); + return backend.IsSupported; + } + catch + { + return false; } } } From 6c934cecd956bb5ab76ca86293c9977ecde6b0d4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 16:05:39 +1000 Subject: [PATCH 125/136] Add missing tests. --- .../Processing/DrawingCanvasTests.Process.cs | 29 ++ .../DrawingCanvasTests.SaveCount.cs | 130 +++++++ .../DrawingCanvasTests.TextMeasuring.cs | 85 +++++ .../Processing/PenTests.cs | 324 ++++++++++++++++++ ...ss_PathBuilder_MatchesReference_Rgba32.png | 3 + 5 files changed, 571 insertions(+) create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index 1f20530f5..cc2986a76 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -12,6 +12,35 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public partial class DrawingCanvasTests { + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + PathBuilder blurBuilder = new(); + blurBuilder.AddArc(new PointF(55, 40), 55, 40, 0, 0, 360); + blurBuilder.CloseAllFigures(); + + PathBuilder pixelateBuilder = new(); + pixelateBuilder.AddLine(110, 80, 220, 80); + pixelateBuilder.AddLine(220, 80, 165, 160); + pixelateBuilder.AddLine(165, 160, 110, 80); + pixelateBuilder.CloseAllFigures(); + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurBuilder, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelateBuilder, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + [Theory] [WithBlankImage(220, 160, PixelTypes.Rgba32)] public void Process_Path_MatchesReference(TestImageProvider provider) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs new file mode 100644 index 000000000..5ddbfc48a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void SaveCount_InitialValue_IsOne() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Save_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + int count1 = canvas.Save(); + Assert.Equal(2, count1); + Assert.Equal(2, canvas.SaveCount); + + int count2 = canvas.Save(); + Assert.Equal(3, count2); + Assert.Equal(3, canvas.SaveCount); + } + + [Fact] + public void SaveWithOptions_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.Save(new DrawingOptions(), new RectangularPolygon(0, 0, 32, 32)); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void Restore_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + _ = canvas.Save(); + Assert.Equal(3, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(2, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Restore_AtRootState_DoesNotDecrementBelowOne() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void RestoreTo_SetsSaveCountToSpecifiedLevel() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + int mid = canvas.Save(); + _ = canvas.Save(); + _ = canvas.Save(); + Assert.Equal(5, canvas.SaveCount); + + canvas.RestoreTo(mid); + Assert.Equal(mid, canvas.SaveCount); + } + + [Fact] + public void RestoreTo_One_RestoresToRoot() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + _ = canvas.Save(); + _ = canvas.Save(); + + canvas.RestoreTo(1); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Save_ReturnValue_MatchesSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + for (int i = 0; i < 5; i++) + { + int returned = canvas.Save(); + Assert.Equal(canvas.SaveCount, returned); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs index 8da535ab8..15f69e270 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs @@ -197,4 +197,89 @@ public void TextMeasuring_RenderedMetrics_MatchesReference(TestImageProv target.DebugSave(provider, appendSourceFileOrDescription: false); target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } + + [Fact] + public void MeasureTextSize_ReturnsNonEmptyRectangle() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF size = canvas.MeasureTextSize(textOptions, "Hello"); + + Assert.True(size.Width > 0, "Width should be positive."); + Assert.True(size.Height > 0, "Height should be positive."); + } + + [Fact] + public void MeasureTextSize_EmptyText_ReturnsEmpty() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF size = canvas.MeasureTextSize(textOptions, ReadOnlySpan.Empty); + + Assert.Equal(RectangleF.Empty, size); + } + + [Fact] + public void MeasureTextSize_LongerText_IsWider() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF shortSize = canvas.MeasureTextSize(textOptions, "Hi"); + RectangleF longSize = canvas.MeasureTextSize(textOptions, "Hello World"); + + Assert.True(longSize.Width > shortSize.Width, "Longer text should produce a wider measurement."); + } + + [Fact] + public void TryMeasureCharacterAdvances_ReturnsAdvancesForEachCharacter() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + const string text = "ABC"; + bool result = canvas.TryMeasureCharacterAdvances(textOptions, text, out ReadOnlySpan advances); + + Assert.True(result); + Assert.Equal(text.Length, advances.Length); + + for (int i = 0; i < advances.Length; i++) + { + Assert.True(advances[i].Bounds.Width > 0, $"Advance width for character {i} should be positive."); + } + } + + [Fact] + public void TryMeasureCharacterAdvances_EmptyText_ReturnsFalse() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + bool result = canvas.TryMeasureCharacterAdvances(textOptions, ReadOnlySpan.Empty, out ReadOnlySpan advances); + + Assert.False(result); + Assert.True(advances.IsEmpty); + } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs new file mode 100644 index 000000000..6486159a8 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class PenTests +{ + // Constructor / property tests + [Fact] + public void SolidPen_ColorConstructor_SetsProperties() + { + SolidPen pen = new(Color.Red); + + Assert.Equal(1, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_ColorWidthConstructor_SetsProperties() + { + SolidPen pen = new(Color.Blue, 5); + + Assert.Equal(5, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_BrushConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Green); + SolidPen pen = new(brush); + + Assert.Equal(1, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_BrushWidthConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Green); + SolidPen pen = new(brush, 7.5F); + + Assert.Equal(7.5F, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_PenOptionsConstructor_SetsProperties() + { + PenOptions options = new(Color.Coral, 4) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round } + }; + + SolidPen pen = new(options); + + Assert.Equal(4, pen.StrokeWidth); + Assert.Equal(LineJoin.Round, pen.StrokeOptions.LineJoin); + } + + [Fact] + public void PatternPen_ColorPatternConstructor_SetsProperties() + { + float[] pattern = [3f, 1f]; + PatternPen pen = new(Color.Black, pattern); + + Assert.Equal(1, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_ColorWidthPatternConstructor_SetsProperties() + { + float[] pattern = [2f, 1f, 1f, 1f]; + PatternPen pen = new(Color.Navy, 3, pattern); + + Assert.Equal(3, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_BrushWidthPatternConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Teal); + float[] pattern = [1f, 1f]; + PatternPen pen = new(brush, 2.5F, pattern); + + Assert.Equal(2.5F, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_PenOptionsConstructor_SetsProperties() + { + float[] pattern = [3f, 1f, 1f, 1f]; + PenOptions options = new(Color.Red, 6, pattern) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Round } + }; + + PatternPen pen = new(options); + + Assert.Equal(6, pen.StrokeWidth); + Assert.Equal(LineCap.Round, pen.StrokeOptions.LineCap); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void SolidPen_DefaultStrokeOptions_UsesDefaults() + { + SolidPen pen = new(Color.Black, 2); + + Assert.Equal(LineJoin.Bevel, pen.StrokeOptions.LineJoin); + Assert.Equal(LineCap.Butt, pen.StrokeOptions.LineCap); + Assert.Equal(InnerJoin.Miter, pen.StrokeOptions.InnerJoin); + Assert.Equal(4D, pen.StrokeOptions.MiterLimit); + } + + [Fact] + public void Pens_Dash_HasExpectedPattern() + { + PatternPen pen = Pens.Dash(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual(new float[] { 3f, 1f })); + } + + [Fact] + public void Pens_Dot_HasExpectedPattern() + { + PatternPen pen = Pens.Dot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual(new float[] { 1f, 1f })); + } + + [Fact] + public void Pens_DashDot_HasExpectedPattern() + { + PatternPen pen = Pens.DashDot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual(new float[] { 3f, 1f, 1f, 1f })); + } + + [Fact] + public void Pens_DashDotDot_HasExpectedPattern() + { + PatternPen pen = Pens.DashDotDot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual(new float[] { 3f, 1f, 1f, 1f, 1f, 1f })); + } + + // Equality tests + [Fact] + public void SolidPen_Equal_WhenSameColorAndWidth() + { + Pen a = Pens.Solid(Color.Red, 3); + Pen b = Pens.Solid(Color.Red, 3); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void SolidPen_NotEqual_WhenDifferentColor() + { + Pen a = Pens.Solid(Color.Red, 3); + Pen b = Pens.Solid(Color.Blue, 3); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void SolidPen_NotEqual_WhenDifferentWidth() + { + Pen a = Pens.Solid(Color.Red, 3); + Pen b = Pens.Solid(Color.Red, 5); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_Equal_WhenSamePattern() + { + Pen a = Pens.Dash(Color.Black, 2); + Pen b = Pens.Dash(Color.Black, 2); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentPattern() + { + Pen a = Pens.Dash(Color.Black, 2); + Pen b = Pens.Dot(Color.Black, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentColor() + { + Pen a = Pens.Dash(Color.Red, 2); + Pen b = Pens.Dash(Color.Green, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentWidth() + { + Pen a = Pens.Dash(Color.Black, 2); + Pen b = Pens.Dash(Color.Black, 4); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void DashDot_Equal_WhenSameParameters() + { + Pen a = Pens.DashDot(Color.Navy, 3); + Pen b = Pens.DashDot(Color.Navy, 3); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void DashDotDot_Equal_WhenSameParameters() + { + Pen a = Pens.DashDotDot(Color.Teal, 1.5F); + Pen b = Pens.DashDotDot(Color.Teal, 1.5F); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void DashDot_NotEqual_ToDashDotDot() + { + Pen a = Pens.DashDot(Color.Black, 2); + Pen b = Pens.DashDotDot(Color.Black, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void SolidPen_NotEqual_ToPatternPen() + { + Pen solid = Pens.Solid(Color.Black, 2); + Pen pattern = Pens.Dash(Color.Black, 2); + + Assert.False(solid.Equals(pattern)); + Assert.False(pattern.Equals(solid)); + } + + [Fact] + public void PatternPen_CustomPattern_Equal_WhenSameValues() + { + float[] pattern = [2f, 1f, 0.5f, 1f]; + Pen a = new PatternPen(Color.Red, 3, pattern); + Pen b = new PatternPen(Color.Red, 3, pattern); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void PatternPen_CustomPattern_NotEqual_WhenDifferentValues() + { + Pen a = new PatternPen(Color.Red, 3, [2f, 1f, 0.5f, 1f]); + Pen b = new PatternPen(Color.Red, 3, [2f, 1f, 1f, 1f]); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_CustomPattern_NotEqual_WhenDifferentLength() + { + Pen a = new PatternPen(Color.Red, 3, [2f, 1f]); + Pen b = new PatternPen(Color.Red, 3, [2f, 1f, 1f]); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void Pen_Equals_Null_ReturnsFalse() + { + Pen pen = Pens.Dash(Color.Black, 2); + + Assert.False(pen.Equals((Pen?)null)); + Assert.False(pen.Equals((object?)null)); + } + + [Fact] + public void Pen_Equals_Object_WhenSame() + { + Pen a = Pens.Dash(Color.Black, 2); + Pen b = Pens.Dash(Color.Black, 2); + + Assert.True(a.Equals((object)b)); + } + + [Fact] + public void PatternPen_WithBrush_Equal_WhenSameBrushAndPattern() + { + Brush brush = Brushes.Solid(Color.Coral); + Pen a = Pens.Dash(brush, 4); + Pen b = Pens.Dash(brush, 4); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } +} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..ed5c8f4a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:005950a4aff24fb25f8a7db370770277c4dbbfafb9e759d21f7a8545bac76941 +size 19785 From 488bed429b2ad3db7c91dd73ba0add6f68dd8964 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 21:56:41 +1000 Subject: [PATCH 126/136] Add GPU layer compositing and shared WGSL snippets --- .../RemoteExecutor/RemoteExecutor.cs | 20 +- .../Shaders/ComposeLayerComputeShader.cs | 220 +++++ .../Shaders/CompositeComputeShader.cs | 116 +-- .../Shaders/CompositionShaderSnippets.cs | 133 +++ .../WEBGPU_BACKEND_PROCESS.md | 125 ++- .../WebGPUDrawingBackend.ComposeLayer.cs | 382 +++++++++ .../WebGPUDrawingBackend.Readback.cs | 7 - .../WebGPUDrawingBackend.cs | 495 +++++------ .../WebGPUFlushContext.cs | 769 +----------------- .../Backends/DEFAULT_DRAWING_BACKEND.md | 249 ++++++ .../Backends/DefaultDrawingBackend.cs | 76 +- .../Processing/Backends/IDrawingBackend.cs | 40 + .../Processing/DRAWING_CANVAS.md | 298 +++++++ .../DrawingCanvasBatcher{TPixel}.cs | 5 + .../Processing/DrawingCanvasState.cs | 58 ++ .../Processing/DrawingCanvas{TPixel}.cs | 131 ++- .../Processing/IDrawingCanvas.cs | 44 + .../Drawing/DrawPolygon.cs | 7 - .../Drawing/DrawTextRepeatedGlyphs.cs | 13 - .../Backends/WebGPUDrawingBackendTests.cs | 653 +++++++-------- .../Processing/DrawingCanvasBatcherTests.cs | 18 + .../Processing/DrawingCanvasTests.Process.cs | 17 + .../DrawingCanvasTests.SaveLayer.cs | 198 +++++ .../RasterizerDefaultsExtensionsTests.cs | 18 + ...ss_PathBuilder_MatchesReference_Rgba32.png | 4 +- ...enderedMetrics_MatchesReference_Rgba32.png | 4 +- .../CanRenderTextOutOfBoundsIssue301.png | 4 +- ...hDiagonalReturnsCorrectImages_TopRight.png | 4 +- ...HorizontalGradientWithRepMode_DontFill.png | 4 +- ...rsCrawl_StarWarsCrawl_WebGPU_CPURegion.png | 3 - ...h_Stroke_LineCap_Butt_WebGPU_CPURegion.png | 3 - ..._Stroke_LineCap_Round_WebGPU_CPURegion.png | 3 - ...Stroke_LineCap_Square_WebGPU_CPURegion.png | 3 - ...Stroke_LineJoin_Bevel_WebGPU_CPURegion.png | 3 - ..._LineJoin_MiterRevert_WebGPU_CPURegion.png | 3 - ...e_LineJoin_MiterRound_WebGPU_CPURegion.png | 3 - ...Stroke_LineJoin_Miter_WebGPU_CPURegion.png | 3 - ...Stroke_LineJoin_Round_WebGPU_CPURegion.png | 3 - ...utput_DrawPath_Stroke_WebGPU_CPURegion.png | 3 - ...atedGlyphs_AfterClear_WebGPU_CPURegion.png | 3 - ...eCache_RepeatedGlyphs_WebGPU_CPURegion.png | 3 - ...aredCoverage_DrawText_WebGPU_CPURegion.png | 3 - ...Path_AliasedThreshold_WebGPU_CPURegion.png | 3 - ...tput_FillPath_EvenOdd_WebGPU_CPURegion.png | 3 - ...llPath_LargeTileCount_WebGPU_CPURegion.png | 3 - ...Path_MultipleSeparate_WebGPU_CPURegion.png | 3 - ...Path_EllipticGradient_WebGPU_CPURegion.png | 3 - ...ipticGradient_Reflect_WebGPU_CPURegion.png | 3 - ...ns_ImageBrush_Add_Src_WebGPU_CPURegion.png | 3 - ...Brush_Darken_DestAtop_WebGPU_CPURegion.png | 3 - ...geBrush_HardLight_Xor_WebGPU_CPURegion.png | 3 - ...eBrush_Lighten_DestIn_WebGPU_CPURegion.png | 3 - ...rush_Multiply_SrcAtop_WebGPU_CPURegion.png | 3 - ...ageBrush_Normal_Clear_WebGPU_CPURegion.png | 3 - ...eBrush_Normal_SrcOver_WebGPU_CPURegion.png | 3 - ...geBrush_Overlay_SrcIn_WebGPU_CPURegion.png | 3 - ...Brush_Screen_DestOver_WebGPU_CPURegion.png | 3 - ...rush_Subtract_DestOut_WebGPU_CPURegion.png | 3 - ...ns_SolidBrush_Add_Src_WebGPU_CPURegion.png | 3 - ...Brush_Darken_DestAtop_WebGPU_CPURegion.png | 3 - ...idBrush_HardLight_Xor_WebGPU_CPURegion.png | 3 - ...dBrush_Lighten_DestIn_WebGPU_CPURegion.png | 3 - ...rush_Multiply_SrcAtop_WebGPU_CPURegion.png | 3 - ...lidBrush_Normal_Clear_WebGPU_CPURegion.png | 3 - ...dBrush_Normal_SrcOver_WebGPU_CPURegion.png | 3 - ...idBrush_Overlay_SrcIn_WebGPU_CPURegion.png | 3 - ...Brush_Screen_DestOver_WebGPU_CPURegion.png | 3 - ...rush_Subtract_DestOut_WebGPU_CPURegion.png | 3 - ...t_FillPath_ImageBrush_WebGPU_CPURegion.png | 3 - ...llPath_LinearGradient_WebGPU_CPURegion.png | 3 - ...LinearGradient_Repeat_WebGPU_CPURegion.png | 3 - ...arGradient_Repeat_WebGPU_NativeSurface.png | 4 +- ...arGradient_ThreePoint_WebGPU_CPURegion.png | 3 - ...NonZeroNestedContours_WebGPU_CPURegion.png | 3 - ...PatternBrush_Diagonal_WebGPU_CPURegion.png | 3 - ...tternBrush_Horizontal_WebGPU_CPURegion.png | 3 - ...RadialGradient_Single_WebGPU_CPURegion.png | 3 - ...ialGradient_TwoCircle_WebGPU_CPURegion.png | 3 - ...FillPath_RecolorBrush_WebGPU_CPURegion.png | 3 - ...illPath_SweepGradient_WebGPU_CPURegion.png | 3 - ...epGradient_PartialArc_WebGPU_CPURegion.png | 3 - ...efaultOutput_FillPath_WebGPU_CPURegion.png | 3 - ...urfaceSubregionParity_WebGPU_CPURegion.png | 3 - ...h_NativeSurfaceParity_WebGPU_CPURegion.png | 3 - ...sults_MultipleFlushes_WebGPU_CPURegion.png | 3 - ...DefaultOutput_Process_WebGPU_CPURegion.png | 3 - ...ltOutput_SaveLayer_FullOpacity_Default.png | 3 + ...Layer_FullOpacity_WebGPU_NativeSurface.png | 3 + ...ltOutput_SaveLayer_HalfOpacity_Default.png | 3 + ...Layer_HalfOpacity_WebGPU_NativeSurface.png | 3 + ...aveLayer_MixedSaveAndSaveLayer_Default.png | 3 + ...dSaveAndSaveLayer_WebGPU_NativeSurface.png | 3 + ...tOutput_SaveLayer_NestedLayers_Default.png | 3 + ...ayer_NestedLayers_WebGPU_NativeSurface.png | 3 + ...Output_SaveLayer_WithBlendMode_Default.png | 3 + ...yer_WithBlendMode_WebGPU_NativeSurface.png | 3 + ...ultOutput_SaveLayer_WithBounds_Default.png | 3 + ...eLayer_WithBounds_WebGPU_NativeSurface.png | 3 + ...d300x300_(255,255,255,255)_scale-0.003.png | 4 +- 99 files changed, 2572 insertions(+), 1752 deletions(-) create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs create mode 100644 src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md create mode 100644 src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png create mode 100644 tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs index 619d58f98..f8749dee5 100644 --- a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs @@ -46,14 +46,22 @@ static RemoteExecutor() if (!IOPath.GetFileName(HostRunner).Equals(hostName, StringComparison.OrdinalIgnoreCase)) { - string runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); - string? directory = IOPath.GetDirectoryName(IOPath.GetDirectoryName(IOPath.GetDirectoryName(runtimeDir))); - if (directory is not null) + // Walk up from the runtime directory to find the dotnet host executable. + // The runtime directory is typically: + // /shared/Microsoft.NETCore.App// + // so dotnet.exe is 3–4 levels up depending on trailing separator. + string? directory = RuntimeEnvironment.GetRuntimeDirectory(); + for (int i = 0; i < 4 && directory is not null; i++) { - string dotnetExe = IOPath.Combine(directory, hostName); - if (File.Exists(dotnetExe)) + directory = IOPath.GetDirectoryName(directory); + if (directory is not null) { - HostRunner = dotnetExe; + string dotnetExe = IOPath.Combine(directory, hostName); + if (File.Exists(dotnetExe)) + { + HostRunner = dotnetExe; + break; + } } } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs new file mode 100644 index 000000000..bb2867cc8 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU compute shader that composites a source layer texture onto a destination texture +/// using configurable blend mode, alpha composition mode, and opacity. +/// +internal static class ComposeLayerComputeShader +{ + private static readonly object CacheSync = new(); + private static readonly Dictionary ShaderCache = []; + + private static readonly string ShaderTemplate = + """ + struct LayerConfig { + source_width: u32, + source_height: u32, + dest_offset_x: i32, + dest_offset_y: i32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + _padding: u32, + }; + + @group(0) @binding(0) var source_texture: texture_2d<__TEXEL_TYPE__>; + @group(0) @binding(1) var backdrop_texture: texture_2d<__TEXEL_TYPE__>; + @group(0) @binding(2) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; + @group(0) @binding(3) var config: LayerConfig; + + __DECODE_TEXEL_FUNCTION__ + + __ENCODE_OUTPUT_FUNCTION__ + + __BLEND_AND_COMPOSE__ + + @compute @workgroup_size(16, 16, 1) + fn cs_main(@builtin(global_invocation_id) gid: vec3) { + // Output coordinates are in local output-texture space. + let out_x = i32(gid.x); + let out_y = i32(gid.y); + + // Destination coordinates map into the full backdrop texture. + let dest_x = out_x + config.dest_offset_x; + let dest_y = out_y + config.dest_offset_y; + + let dest_dims = textureDimensions(backdrop_texture); + if (dest_x < 0 || dest_y < 0 || u32(dest_x) >= dest_dims.x || u32(dest_y) >= dest_dims.y) { + return; + } + + let src_x = out_x; + let src_y = out_y; + if (u32(src_x) >= config.source_width || u32(src_y) >= config.source_height) { + // Outside layer bounds — pass through the backdrop. + let backdrop = decode_texel(__LOAD_BACKDROP__); + let alpha = backdrop.a; + let rgb = unpremultiply(backdrop.rgb, alpha); + __STORE_OUTPUT__ + return; + } + + let backdrop = decode_texel(__LOAD_BACKDROP__); + let source_raw = decode_texel(__LOAD_SOURCE__); + + // Apply layer opacity. + let opacity = bitcast(config.blend_percentage); + let source = vec4(source_raw.rgb, source_raw.a * opacity); + + let result = compose_pixel(backdrop, source, config.color_blend_mode, config.alpha_composition_mode); + let alpha = result.a; + let rgb = unpremultiply(result.rgb, alpha); + __STORE_OUTPUT__ + } + """; + + /// + /// Gets the null-terminated WGSL source for the layer composite shader variant. + /// + public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error) + { + if (!CompositeComputeShader.TryGetInputSampleType(textureFormat, out _)) + { + code = []; + error = $"Layer composite shader does not support texture format '{textureFormat}'."; + return false; + } + + lock (CacheSync) + { + if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) + { + code = cachedCode; + error = null; + return true; + } + + LayerShaderTraits traits = GetTraits(textureFormat); + string source = ShaderTemplate + .Replace("__TEXEL_TYPE__", traits.TexelType, StringComparison.Ordinal) + .Replace("__OUTPUT_FORMAT__", traits.OutputFormat, StringComparison.Ordinal) + .Replace("__DECODE_TEXEL_FUNCTION__", traits.DecodeTexelFunction, StringComparison.Ordinal) + .Replace("__ENCODE_OUTPUT_FUNCTION__", traits.EncodeOutputFunction, StringComparison.Ordinal) + .Replace("__BLEND_AND_COMPOSE__", CompositionShaderSnippets.BlendAndCompose, StringComparison.Ordinal) + .Replace("__LOAD_BACKDROP__", traits.LoadBackdropExpression, StringComparison.Ordinal) + .Replace("__LOAD_SOURCE__", traits.LoadSourceExpression, StringComparison.Ordinal) + .Replace("__STORE_OUTPUT__", traits.StoreOutputStatement, StringComparison.Ordinal); + + byte[] sourceBytes = Encoding.UTF8.GetBytes(source); + code = new byte[sourceBytes.Length + 1]; + sourceBytes.CopyTo(code, 0); + code[^1] = 0; + ShaderCache[textureFormat] = code; + } + + error = null; + return true; + } + + private static LayerShaderTraits GetTraits(TextureFormat textureFormat) + { + return textureFormat switch + { + TextureFormat.R8Unorm => CreateFloatTraits("r8unorm"), + TextureFormat.RG8Unorm => CreateFloatTraits("rg8unorm"), + TextureFormat.Rgba8Unorm => CreateFloatTraits("rgba8unorm"), + TextureFormat.Bgra8Unorm => CreateFloatTraits("bgra8unorm"), + TextureFormat.Rgb10A2Unorm => CreateFloatTraits("rgb10a2unorm"), + TextureFormat.R16float => CreateFloatTraits("r16float"), + TextureFormat.RG16float => CreateFloatTraits("rg16float"), + TextureFormat.Rgba16float => CreateFloatTraits("rgba16float"), + TextureFormat.Rgba32float => CreateFloatTraits("rgba32float"), + TextureFormat.RG8Snorm => CreateSnormTraits("rg8snorm"), + TextureFormat.Rgba8Snorm => CreateSnormTraits("rgba8snorm"), + _ => CreateFloatTraits("rgba8unorm"), + }; + } + + private static LayerShaderTraits CreateFloatTraits(string outputFormat) + { + const string decodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return texel; + } + """; + + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + return color; + } + """; + + return new LayerShaderTraits( + outputFormat, + "f32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private static LayerShaderTraits CreateSnormTraits(string outputFormat) + { + const string decodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return (texel * 0.5) + vec4(0.5); + } + """; + + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return (clamped * 2.0) - vec4(1.0); + } + """; + + return new LayerShaderTraits( + outputFormat, + "f32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private readonly struct LayerShaderTraits( + string outputFormat, + string texelType, + string decodeTexelFunction, + string encodeOutputFunction, + string loadBackdropExpression, + string loadSourceExpression, + string storeOutputStatement) + { + public string OutputFormat { get; } = outputFormat; + + public string TexelType { get; } = texelType; + + public string DecodeTexelFunction { get; } = decodeTexelFunction; + + public string EncodeOutputFunction { get; } = encodeOutputFunction; + + public string LoadBackdropExpression { get; } = loadBackdropExpression; + + public string LoadSourceExpression { get; } = loadSourceExpression; + + public string StoreOutputStatement { get; } = storeOutputStatement; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs index 40eeff8db..93e9d526c 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs @@ -321,120 +321,7 @@ fn sweep_gradient_t(x: f32, y: f32, cmd: Params) -> f32 { __ENCODE_OUTPUT_FUNCTION__ - fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { - if (alpha <= 0.0) { - return vec3(0.0); - } - - return rgb / alpha; - } - - fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { - switch mode { - case 1u: { - return backdrop * source; - } - case 2u: { - return backdrop + source; - } - case 3u: { - return backdrop - source; - } - case 4u: { - return 1.0 - ((1.0 - backdrop) * (1.0 - source)); - } - case 5u: { - return min(backdrop, source); - } - case 6u: { - return max(backdrop, source); - } - case 7u: { - return select( - 2.0 * backdrop * source, - 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), - backdrop >= vec3(0.5)); - } - case 8u: { - return select( - 2.0 * backdrop * source, - 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), - source >= vec3(0.5)); - } - default: { - return source; - } - } - } - - fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { - let destination_alpha = destination_premul.a; - let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); - let source_alpha = source.a; - let source_rgb = source.rgb; - let source_premul = source_rgb * source_alpha; - let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); - let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); - let shared_alpha = source_alpha * destination_alpha; - - switch alpha_mode { - case 1u: { - return vec4(source_premul, source_alpha); - } - case 2u: { - let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); - return vec4(premul, destination_alpha); - } - case 3u: { - let alpha = source_alpha * destination_alpha; - return vec4(source_premul * destination_alpha, alpha); - } - case 4u: { - let alpha = source_alpha * (1.0 - destination_alpha); - return vec4(source_premul * (1.0 - destination_alpha), alpha); - } - case 5u: { - return destination_premul; - } - case 6u: { - let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); - return vec4(premul, source_alpha); - } - case 7u: { - let alpha = destination_alpha + source_alpha - shared_alpha; - let premul = - (source_rgb * (source_alpha - shared_alpha)) + - (destination_rgb_straight * (destination_alpha - shared_alpha)) + - (reverse_blend * shared_alpha); - return vec4(premul, alpha); - } - case 8u: { - let alpha = destination_alpha * source_alpha; - return vec4(destination_premul.rgb * source_alpha, alpha); - } - case 9u: { - let alpha = destination_alpha * (1.0 - source_alpha); - return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); - } - case 10u: { - return vec4(0.0, 0.0, 0.0, 0.0); - } - case 11u: { - let source_term = source_premul * (1.0 - destination_alpha); - let destination_term = destination_premul.rgb * (1.0 - source_alpha); - let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); - return vec4(source_term + destination_term, alpha); - } - default: { - let alpha = source_alpha + destination_alpha - shared_alpha; - let premul = - (destination_rgb_straight * (destination_alpha - shared_alpha)) + - (source_rgb * (source_alpha - shared_alpha)) + - (forward_blend * shared_alpha); - return vec4(premul, alpha); - } - } - } + __BLEND_AND_COMPOSE__ fn positive_mod(value: i32, divisor: i32) -> i32 { let m = value % divisor; @@ -1296,6 +1183,7 @@ public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out .Replace("__OUTPUT_FORMAT__", traits.OutputFormat, StringComparison.Ordinal) .Replace("__DECODE_TEXEL_FUNCTION__", traits.DecodeTexelFunction, StringComparison.Ordinal) .Replace("__ENCODE_OUTPUT_FUNCTION__", traits.EncodeOutputFunction, StringComparison.Ordinal) + .Replace("__BLEND_AND_COMPOSE__", CompositionShaderSnippets.BlendAndCompose, StringComparison.Ordinal) .Replace("__LOAD_BACKDROP__", traits.LoadBackdropExpression, StringComparison.Ordinal) .Replace("__LOAD_BRUSH__", traits.LoadBrushExpression, StringComparison.Ordinal) .Replace("__STORE_OUTPUT__", traits.StoreOutputStatement, StringComparison.Ordinal); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs new file mode 100644 index 000000000..cca89d98d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Shared WGSL function snippets used by multiple compute shaders that perform pixel blending +/// and alpha composition (e.g., and ). +/// +internal static class CompositionShaderSnippets +{ + /// + /// WGSL functions for unpremultiplying alpha, blending colors by mode, + /// and compositing pixels using Porter-Duff alpha composition. + /// + internal const string BlendAndCompose = + """ + fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { + if (alpha <= 0.0) { + return vec3(0.0); + } + + return rgb / alpha; + } + + fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { + switch mode { + case 1u: { + return backdrop * source; + } + case 2u: { + return backdrop + source; + } + case 3u: { + return backdrop - source; + } + case 4u: { + return 1.0 - ((1.0 - backdrop) * (1.0 - source)); + } + case 5u: { + return min(backdrop, source); + } + case 6u: { + return max(backdrop, source); + } + case 7u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + backdrop >= vec3(0.5)); + } + case 8u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + source >= vec3(0.5)); + } + default: { + return source; + } + } + } + + fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { + let destination_alpha = destination_premul.a; + let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); + let source_alpha = source.a; + let source_rgb = source.rgb; + let source_premul = source_rgb * source_alpha; + let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); + let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); + let shared_alpha = source_alpha * destination_alpha; + + switch alpha_mode { + case 1u: { + return vec4(source_premul, source_alpha); + } + case 2u: { + let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); + return vec4(premul, destination_alpha); + } + case 3u: { + let alpha = source_alpha * destination_alpha; + return vec4(source_premul * destination_alpha, alpha); + } + case 4u: { + let alpha = source_alpha * (1.0 - destination_alpha); + return vec4(source_premul * (1.0 - destination_alpha), alpha); + } + case 5u: { + return destination_premul; + } + case 6u: { + let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); + return vec4(premul, source_alpha); + } + case 7u: { + let alpha = destination_alpha + source_alpha - shared_alpha; + let premul = + (source_rgb * (source_alpha - shared_alpha)) + + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (reverse_blend * shared_alpha); + return vec4(premul, alpha); + } + case 8u: { + let alpha = destination_alpha * source_alpha; + return vec4(destination_premul.rgb * source_alpha, alpha); + } + case 9u: { + let alpha = destination_alpha * (1.0 - source_alpha); + return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); + } + case 10u: { + return vec4(0.0, 0.0, 0.0, 0.0); + } + case 11u: { + let source_term = source_premul * (1.0 - destination_alpha); + let destination_term = destination_premul.rgb * (1.0 - source_alpha); + let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); + return vec4(source_term + destination_term, alpha); + } + default: { + let alpha = source_alpha + destination_alpha - shared_alpha; + let premul = + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (source_rgb * (source_alpha - shared_alpha)) + + (forward_blend * shared_alpha); + return vec4(premul, alpha); + } + } + } + """; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index 0bdf34563..62fc7f624 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -1,16 +1,17 @@ # WebGPU Backend Process -This document describes the current runtime flow used by `WebGPUDrawingBackend` when flushing a `CompositionScene`. +This document describes the runtime flows used by `WebGPUDrawingBackend` for scene flushing and layer compositing. ## End-to-End Flow ```text DrawingCanvasBatcher.Flush() -> IDrawingBackend.FlushCompositions(scene) + -> if target has no NativeSurface: delegate to DefaultDrawingBackend directly -> capability checks -> TryGetCompositeTextureFormat -> AreAllCompositionBrushesSupported - -> if unsupported: scene-scoped fallback (DefaultDrawingBackend) + -> if unsupported: staging fallback (see Fallback Behavior) -> CompositionScenePlanner.CreatePreparedBatches(commands, targetBounds) -> clip each command to target bounds -> group contiguous commands by DefinitionKey @@ -57,13 +58,9 @@ DrawingCanvasBatcher.Flush() -> samples brush (see Brush Types below) -> composes pixel using Porter-Duff alpha composition + color blend mode -> writes final pixel to output texture - -> destination writeback: - -> NativeSurface: copy output texture region into target texture - -> CPU Region: set ReadbackSourceOverride to output texture (skip extra copy) + -> copy composited output texture region back into target texture -> TryFinalizeFlush - -> NativeSurface: finish encoder + single QueueSubmit (non-blocking) - -> CPU Region: encode texture->buffer copy, finish encoder, QueueSubmit, - synchronous BufferMapAsync + poll wait, copy mapped bytes to CPU region + -> finish encoder + single QueueSubmit (non-blocking) -> on any GPU failure: scene-scoped fallback (DefaultDrawingBackend) ``` @@ -163,23 +160,115 @@ Color stops are sorted by ratio in the `GradientBrush` constructor using a stabl ## Destination Writeback - `FlushCompositions` performs one command-buffer submission (`QueueSubmit`) per scene flush. -- NativeSurface targets: one GPU-side `CommandEncoderCopyTextureToTexture` from output into the target at composition bounds. No CPU stall. -- CPU Region targets: readback from the output texture directly (skipping the output-to-target copy). Uses `CommandEncoderCopyTextureToBuffer`, `QueueSubmit`, synchronous `BufferMapAsync` with device polling, then copies mapped bytes to the CPU `Buffer2DRegion`. +- One GPU-side `CommandEncoderCopyTextureToTexture` from output into the target at composition bounds. No CPU stall. +- The WebGPU backend only operates on native GPU surfaces. CPU-backed frames are handled entirely by `DefaultDrawingBackend`. ## Fallback Behavior -Fallback is scene-scoped and triggered when: -- The pixel format has no supported WebGPU texture format mapping. -- Any command uses an unsupported brush type (`PathGradientBrush` is not GPU-composable; all other brush types are supported). -- Any GPU operation fails during the flush. +### FlushCompositions -Fallback path: -- If target exposes a CPU region: run `DefaultDrawingBackend.FlushCompositions(...)` directly. -- If target is native-surface only: rent CPU staging frame, run fallback on staging, upload staging pixels back to native target texture. +1. **Non-native target**: If the target frame has no `NativeSurface`, delegate directly + to `DefaultDrawingBackend.FlushCompositions` (no staging, no upload). +2. **Native target, unsupported scene**: Pixel format has no GPU mapping, or a command + uses an unsupported brush (`PathGradientBrush`). Fallback: allocate a clean CPU + staging `Buffer2D`, run `DefaultDrawingBackend.FlushCompositions` on it, then + upload the staging region to the native target texture via `UploadTextureFromRegion`. +3. **GPU failure**: Any GPU operation fails during `TryRenderPreparedFlush` or + `TryFinalizeFlush`. Same staging + upload fallback as (2). + +### ComposeLayer + +1. **Non-native destination**: Delegate directly to `DefaultDrawingBackend.ComposeLayer`. +2. **Native destination, GPU compose fails**: Read both destination and source textures + from the GPU via `TryReadRegion`, compose on CPU via `DefaultDrawingBackend.ComposeLayer`, + then upload the composited destination back to the native texture. + +## Layer Compositing (ComposeLayer) + +`SaveLayer` on `DrawingCanvas` calls `IDrawingBackend.CreateLayerFrame` to allocate the layer. +When the parent target is a native GPU surface, `WebGPUDrawingBackend` creates a GPU-resident +texture for the layer (zero-copy). For CPU-backed parents it falls back to `DefaultDrawingBackend`. +On `Restore`, `IDrawingBackend.ComposeLayer` blends the layer onto the parent target. + +```text +DrawingCanvas.SaveLayer() + -> IDrawingBackend.CreateLayerFrame(parentTarget, width, height) + -> WebGPUDrawingBackend: GPU texture if parent is native, else CPU fallback + -> redirect draw commands to layer frame + +DrawingCanvas.Restore() / RestoreTo() + -> CompositeAndPopLayer(layerState) + -> Flush() current layer batcher + -> pop LayerData from layerDataStack + -> restore parent batcher + -> IDrawingBackend.ComposeLayer(source=layerFrame, destination=parentFrame, offset, options) + -> WebGPUDrawingBackend.ComposeLayer + -> TryComposeLayerGpu (requires destination with NativeSurface) + -> TryGetCompositeTextureFormat + -> destination must expose NativeSurface with WebGPU capability + -> TryAcquireSourceTexture: bind native GPU texture directly (zero-copy) + or upload CPU pixels to temporary GPU texture + -> create output texture sized to composite region + -> ComposeLayerComputeShader dispatch: + -> workgroup size 16x16 + -> each thread reads backdrop at (out_x + offset, out_y + offset) + -> reads source at (out_x, out_y) + -> applies layer opacity, blend mode, alpha composition + -> writes result to output at (out_x, out_y) + -> copy output back to destination texture at compositing offset + -> QueueSubmit + -> fallback: ComposeLayerFallback + -> TryReadRegion(destination) -> CPU staging image + -> TryReadRegion(source) -> CPU staging image + -> DefaultDrawingBackend.ComposeLayer on staging frames + -> UploadTextureFromRegion back to destination texture + -> IDrawingBackend.ReleaseFrameResources(layerFrame) + -> GPU frames: release texture + texture view + -> CPU frames: dispose Buffer2D +``` + +### Shader Bindings (ComposeLayerComputeShader) + +| Binding | Type | Description | +|---|---|---| +| 0 | `texture_2d` | Source layer texture (read) | +| 1 | `texture_2d` | Backdrop/destination texture (read) | +| 2 | `texture_storage_2d, write` | Output texture | +| 3 | `uniform` | `LayerConfig` (source dims, dest offset, blend mode, opacity) | + +### LayerConfig Uniform + +| Field | Type | Description | +|---|---|---| +| source_width, source_height | u32 | Source layer dimensions | +| dest_offset_x, dest_offset_y | i32 | Offset into backdrop texture | +| color_blend_mode | u32 | Porter-Duff color blend mode | +| alpha_composition_mode | u32 | Porter-Duff alpha composition mode | +| blend_percentage | u32 | Layer opacity (f32 bitcast to u32) | + +### Coordinate Spaces + +The output texture is sized to the composite region (intersection of source and +destination bounds). The shader uses local coordinates `(out_x, out_y)` for source +reads and output writes, and offset coordinates `(out_x + dest_offset, out_y + dest_offset)` +for backdrop reads. The final `CopyTextureRegion` places the output at the +compositing offset in the destination texture. + +## Shared Composition Functions + +`CompositionShaderSnippets.BlendAndCompose` contains shared WGSL functions used by both +`CompositeComputeShader` and `ComposeLayerComputeShader`: + +- `unpremultiply(rgb, alpha)` — converts premultiplied alpha to straight alpha +- `blend_color(backdrop, source, mode)` — 8 color blend modes (Normal, Multiply, Add, Subtract, Screen, Darken, Lighten, Overlay, HardLight) +- `compose_pixel(backdrop, source, blend_mode, compose_mode)` — full Porter-Duff alpha composition with 12 modes (Clear, Src, Dest, SrcOver, DestOver, SrcIn, DestIn, SrcOut, DestOut, SrcAtop, DestAtop, Xor) + +Both shaders include these functions via the `__BLEND_AND_COMPOSE__` template placeholder, +avoiding code duplication between the rasterize+composite and layer composite pipelines. ## Shader Source -`CompositeComputeShader` generates WGSL source per target texture format at runtime, substituting format-specific template tokens for texel decode/encode, backdrop/brush load, and output store. Generated source is cached by `TextureFormat` as null-terminated UTF-8 bytes. +`CompositeComputeShader` generates WGSL source per target texture format at runtime, substituting format-specific template tokens for texel decode/encode, backdrop/brush load, and output store. `ComposeLayerComputeShader` uses the same template approach with its own format traits. Generated source is cached by `TextureFormat` as null-terminated UTF-8 bytes. The following static WGSL shaders exist for the legacy CSR GPU pipeline but are not used in the current dispatch path (CSR is computed on CPU): - `CsrCountComputeShader`, `CsrScatterComputeShader` diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs new file mode 100644 index 000000000..1d245f86f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs @@ -0,0 +1,382 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU layer compositing via . +/// +public sealed unsafe partial class WebGPUDrawingBackend +{ + private const string ComposeLayerPipelineKey = "compose-layer"; + private const string ComposeLayerConfigBufferKey = "compose-layer/config"; + + /// + /// Attempts to composite a source layer onto a destination using a GPU compute shader. + /// Returns when GPU compositing is not available — the caller + /// should fall back to for + /// CPU-backed destinations where the transfer overhead outweighs the GPU benefit. + /// + private bool TryComposeLayerGpu( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)) + { + return false; + } + + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + + // Only use GPU compositing when the destination is a native surface. + // CPU-backed destinations fall back to DefaultDrawingBackend where a + // simple pixel blend avoids the upload/readback transfer overhead. + if (!destination.TryGetNativeSurface(out NativeSurface? nativeSurface)) + { + return false; + } + + int srcWidth = source.Bounds.Width; + int srcHeight = source.Bounds.Height; + if (srcWidth <= 0 || srcHeight <= 0) + { + return true; // Nothing to composite. + } + + if (!ComposeLayerComputeShader.TryGetCode(textureFormat, out byte[] shaderCode, out _)) + { + return false; + } + + // TryGetCode already validates format support via TryGetInputSampleType internally. + _ = CompositeComputeShader.TryGetInputSampleType(textureFormat, out TextureSampleType inputSampleType); + + // Create a flush context against the destination surface. + WebGPUFlushContext? flushContext = WebGPUFlushContext.Create( + destination, + textureFormat, + requiredFeature, + configuration.MemoryAllocator); + + if (flushContext is null) + { + return false; + } + + try + { + if (!flushContext.EnsureCommandEncoder()) + { + return false; + } + + // Acquire the source texture: bind the existing GPU texture if native, + // otherwise upload from CPU pixels. + if (!TryAcquireSourceTexture( + flushContext, + source, + configuration.MemoryAllocator, + out Texture* sourceTexture, + out TextureView* sourceTextureView)) + { + return false; + } + + // The destination texture/view are guaranteed valid by WebGPUFlushContext.Create. + Texture* destTexture = flushContext.TargetTexture; + TextureView* destTextureView = flushContext.TargetView; + + // Create output texture sized to the destination. + int destWidth = destination.Bounds.Width; + int destHeight = destination.Bounds.Height; + + // Clamp compositing region to both source and destination bounds. + int startX = Math.Max(0, -destinationOffset.X); + int startY = Math.Max(0, -destinationOffset.Y); + int endX = Math.Min(srcWidth, destWidth - destinationOffset.X); + int endY = Math.Min(srcHeight, destHeight - destinationOffset.Y); + + if (endX <= startX || endY <= startY) + { + return true; // No overlap. + } + + int compositeWidth = endX - startX; + int compositeHeight = endY - startY; + + if (!TryCreateCompositionTexture(flushContext, compositeWidth, compositeHeight, out Texture* outputTexture, out TextureView* outputTextureView, out _)) + { + return false; + } + + // Get or create the compute pipeline. + string pipelineKey = $"{ComposeLayerPipelineKey}/{textureFormat}"; + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + => TryCreateComposeLayerBindGroupLayout( + api, + device, + textureFormat, + inputSampleType, + out layout, + out layoutError); + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + pipelineKey, + shaderCode, + LayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out _)) + { + return false; + } + + // Create and upload the config uniform. + nuint configSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + ComposeLayerConfigBufferKey, + BufferUsage.Uniform | BufferUsage.CopyDst, + configSize, + out WgpuBuffer* configBuffer, + out _, + out _)) + { + return false; + } + + LayerConfigGpu config = new() + { + SourceWidth = (uint)srcWidth, + SourceHeight = (uint)srcHeight, + DestOffsetX = destinationOffset.X + startX, + DestOffsetY = destinationOffset.Y + startY, + ColorBlendMode = (uint)options.ColorBlendingMode, + AlphaCompositionMode = (uint)options.AlphaCompositionMode, + BlendPercentage = FloatToUInt32Bits(options.BlendPercentage), + Padding = 0 + }; + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, configSize); + + // Create bind group. + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[4]; + bindGroupEntries[0] = new BindGroupEntry { Binding = 0, TextureView = sourceTextureView }; + bindGroupEntries[1] = new BindGroupEntry { Binding = 1, TextureView = destTextureView }; + bindGroupEntries[2] = new BindGroupEntry { Binding = 2, TextureView = outputTextureView }; + bindGroupEntries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = configBuffer, + Offset = 0, + Size = configSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 4, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + return false; + } + + flushContext.TrackBindGroup(bindGroup); + + // Dispatch compute. + uint tileCountX = DivideRoundUp(compositeWidth, CompositeTileWidth); + uint tileCountY = DivideRoundUp(compositeHeight, CompositeTileHeight); + + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, tileCountX, tileCountY, 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + // Copy output back to destination texture at the compositing offset. + CopyTextureRegion( + flushContext, + outputTexture, + 0, + 0, + destTexture, + destinationOffset.X + startX, + destinationOffset.Y + startY, + compositeWidth, + compositeHeight); + + return TrySubmit(flushContext); + } + finally + { + flushContext.Dispose(); + } + } + + /// + /// Creates the bind-group layout for the layer compositing compute shader. + /// + private static bool TryCreateComposeLayerBindGroupLayout( + WebGPU api, + Device* device, + TextureFormat outputTextureFormat, + TextureSampleType inputTextureSampleType, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + + // Binding 0: source layer texture (read). + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + // Binding 1: destination/backdrop texture (read). + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + // Binding 2: output texture (write storage). + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + StorageTexture = new StorageTextureBindingLayout + { + Access = StorageTextureAccess.WriteOnly, + Format = outputTextureFormat, + ViewDimension = TextureViewDimension.Dimension2D + } + }; + + // Binding 3: uniform config buffer. + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create compose-layer bind group layout."; + return false; + } + + error = null; + return true; + } + + /// + /// Acquires a GPU texture and view for the source frame. If the source is already + /// a native GPU texture it is bound directly; otherwise CPU pixels are uploaded + /// to a temporary texture. + /// + private static bool TryAcquireSourceTexture( + WebGPUFlushContext flushContext, + ICanvasFrame source, + MemoryAllocator memoryAllocator, + out Texture* sourceTexture, + out TextureView* sourceTextureView) + where TPixel : unmanaged, IPixel + { + if (source.TryGetNativeSurface(out NativeSurface? sourceSurface) + && sourceSurface.TryGetCapability(out WebGPUSurfaceCapability? srcCapability)) + { + sourceTexture = (Texture*)srcCapability.TargetTexture; + sourceTextureView = (TextureView*)srcCapability.TargetTextureView; + return true; + } + + if (source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + if (!TryCreateCompositionTexture(flushContext, sourceRegion.Width, sourceRegion.Height, out sourceTexture, out sourceTextureView, out _)) + { + return false; + } + + WebGPUFlushContext.UploadTextureFromRegion( + flushContext.Api, + flushContext.Queue, + sourceTexture, + sourceRegion, + memoryAllocator); + return true; + } + + sourceTexture = null; + sourceTextureView = null; + return false; + } + + /// + /// GPU uniform config matching the WGSL LayerConfig struct layout. + /// + [StructLayout(LayoutKind.Sequential)] + private struct LayerConfigGpu + { + public uint SourceWidth; + public uint SourceHeight; + public int DestOffsetX; + public int DestOffsetY; + public uint ColorBlendMode; + public uint AlphaCompositionMode; + public uint BlendPercentage; + public uint Padding; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs index aa4044767..028f921b8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -32,13 +32,6 @@ public bool TryReadRegion( image = null; - // When a CPU-backed frame is used with this backend (for example in parity tests), - // delegate to the default CPU readback implementation. - if (target.TryGetCpuRegion(out _)) - { - return this.fallbackBackend.TryReadRegion(configuration, target, sourceRectangle, out image); - } - // Readback is only available for native WebGPU targets with valid interop handles. if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability) || diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 4e1dd1fd8..d52b4165d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Silk.NET.WebGPU; -using Silk.NET.WebGPU.Extensions.WGPU; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -27,9 +26,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// CompositionScene /// -> Encoded scene stream (draw tags + draw-data stream) -/// -> Acquire flush context +/// -> Acquire flush context (native GPU surface only) /// -> Execute one tiled scene pass (binning -> coarse -> fine) -/// -> Blit once and optionally read back to CPU region +/// -> Blit composited output back to target texture /// -> On failure: delegate scene to DefaultDrawingBackend /// /// @@ -46,7 +45,6 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp private const string StrokeExpandConfigBufferKey = "stroke-expand/config"; private const string StrokeExpandCounterBufferKey = "stroke-expand/counter"; private const int UniformBufferOffsetAlignment = 256; - private const int CallbackTimeoutMilliseconds = 10_000; private readonly DefaultDrawingBackend fallbackBackend; private static bool? isSupported; @@ -306,18 +304,26 @@ public void FlushCompositions( return; } + // CPU-backed target — delegate directly to the CPU backend. + if (!target.TryGetNativeSurface(out _)) + { + this.fallbackBackend.FlushCompositions(configuration, target, compositionScene); + return; + } + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature) || !AreAllCompositionBrushesSupported(compositionScene.Commands)) { int fallbackCommandCount = compositionScene.Commands.Count; this.TestingFallbackPrepareCoverageCallCount += fallbackCommandCount; this.TestingFallbackCompositeCoverageCallCount += fallbackCommandCount; + this.FlushCompositionsFallback( configuration, target, compositionScene, - target.TryGetCpuRegion(out Buffer2DRegion _), compositionBounds: null); + return; } @@ -366,7 +372,6 @@ public void FlushCompositions( this.TestingCompositeCoverageCallCount += commandCount; - bool hasCpuRegion = target.TryGetCpuRegion(out Buffer2DRegion cpuRegion); compositionBounds = Rectangle.Intersect( compositionBounds.Value, new Rectangle(0, 0, target.Bounds.Width, target.Bounds.Height)); @@ -378,15 +383,12 @@ public void FlushCompositions( bool gpuSuccess = false; bool gpuReady = false; string? failure = null; - int pixelSizeInBytes = Unsafe.SizeOf(); WebGPUFlushContext? flushContext = WebGPUFlushContext.Create( target, textureFormat, requiredFeature, - pixelSizeInBytes, - configuration.MemoryAllocator, - compositionBounds); + configuration.MemoryAllocator); if (flushContext is null) { @@ -396,7 +398,6 @@ public void FlushCompositions( configuration, target, compositionScene, - target.TryGetCpuRegion(out Buffer2DRegion _), compositionBounds); return; } @@ -417,7 +418,7 @@ public void FlushCompositions( out Rectangle effectiveBounds, out failure); - bool finalizeOk = renderOk && this.TryFinalizeFlush(flushContext, cpuRegion, effectiveBounds); + bool finalizeOk = renderOk && TryFinalizeFlush(flushContext); gpuSuccess = finalizeOk; } @@ -450,18 +451,128 @@ public void FlushCompositions( configuration, target, compositionScene, - hasCpuRegion, compositionBounds); } + /// + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + + // Try GPU texture allocation when the parent target has a native WebGPU surface. + if (TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature) + && parentTarget.TryGetNativeSurface(out NativeSurface? parentSurface)) + { + _ = parentSurface.TryGetCapability(out WebGPUSurfaceCapability? parentCapability); + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + Device* device = (Device*)parentCapability!.Device; + + WebGPUFlushContext.DeviceSharedState deviceState = WebGPUFlushContext.GetOrCreateDeviceState(api, device); + if (requiredFeature == FeatureName.Undefined || deviceState.HasFeature(requiredFeature)) + { + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* texture = api.DeviceCreateTexture(device, in textureDescriptor); + if (texture is not null) + { + TextureViewDescriptor viewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* textureView = api.TextureCreateView(texture, in viewDescriptor); + if (textureView is not null) + { + NativeSurface surface = WebGPUNativeSurfaceFactory.Create( + parentCapability.Device, + parentCapability.Queue, + (nint)texture, + (nint)textureView, + formatId, + width, + height); + + return new NativeCanvasFrame(new Rectangle(0, 0, width, height), surface); + } + + api.TextureRelease(texture); + } + } + } + + // Fall back to CPU allocation. + return this.fallbackBackend.CreateLayerFrame(configuration, parentTarget, width, height); + } + + /// + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + + // CPU-backed destination — delegate directly. + if (!destination.TryGetNativeSurface(out _)) + { + this.fallbackBackend.ComposeLayer(configuration, source, destination, destinationOffset, options); + return; + } + + // Try the GPU compute path first. + if (this.TryComposeLayerGpu(configuration, source, destination, destinationOffset, options)) + { + return; + } + + // GPU path unavailable — stage through CPU and upload. + this.ComposeLayerFallback(configuration, source, destination, destinationOffset, options); + } + /// public void ReleaseFrameResources( Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { - nint targetIdentity = RuntimeHelpers.GetHashCode(target); - WebGPUFlushContext.ReleaseCpuTargetEntries(targetIdentity); + // Release GPU texture resources for layer frames created by this backend. + if (target.TryGetNativeSurface(out NativeSurface? nativeSurface)) + { + _ = nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability); + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + api.TextureViewRelease((TextureView*)capability!.TargetTextureView); + api.TextureRelease((Texture*)capability.TargetTexture); + } + else + { + // CPU-backed frame: delegate cleanup to the fallback backend. + this.fallbackBackend.ReleaseFrameResources(configuration, target); + } } /// @@ -498,62 +609,89 @@ or PatternBrush or RecolorBrush; /// - /// Executes the scene on the CPU fallback backend. + /// Executes the scene on the CPU fallback backend, then uploads the result + /// to the native GPU surface. /// - /// The destination pixel format. - /// The active processing configuration. - /// The original destination frame. - /// The scene to execute. - /// - /// Indicates whether exposes CPU pixels directly. When , - /// a temporary staging frame is composed and uploaded to the native surface. - /// - /// The destination-local bounds touched by the scene when known. private void FlushCompositionsFallback( Configuration configuration, ICanvasFrame target, CompositionScene compositionScene, - bool hasCpuRegion, Rectangle? compositionBounds) where TPixel : unmanaged, IPixel { - if (hasCpuRegion) - { - this.fallbackBackend.FlushCompositions(configuration, target, compositionScene); - return; - } + _ = target.TryGetNativeSurface(out NativeSurface? nativeSurface); + _ = nativeSurface!.TryGetCapability(out WebGPUSurfaceCapability? capability); Rectangle targetBounds = target.Bounds; - using WebGPUFlushContext.FallbackStagingLease stagingLease = - WebGPUFlushContext.RentFallbackStaging(configuration.MemoryAllocator, in targetBounds); + using Buffer2D stagingBuffer = + configuration.MemoryAllocator.Allocate2D(targetBounds.Width, targetBounds.Height, AllocationOptions.Clean); - Buffer2DRegion stagingRegion = stagingLease.Region; + Buffer2DRegion stagingRegion = new(stagingBuffer); ICanvasFrame stagingFrame = new MemoryCanvasFrame(stagingRegion); + this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene); - using WebGPUFlushContext uploadContext = WebGPUFlushContext.CreateUploadContext(target, configuration.MemoryAllocator); - if (compositionBounds is Rectangle uploadBounds && - uploadBounds.Width > 0 && - uploadBounds.Height > 0) + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + Buffer2DRegion uploadRegion = compositionBounds is Rectangle cb && cb.Width > 0 && cb.Height > 0 + ? stagingRegion.GetSubRegion(cb) + : stagingRegion; + + uint destX = compositionBounds is Rectangle cbx ? (uint)cbx.X : 0; + uint destY = compositionBounds is Rectangle cby ? (uint)cby.Y : 0; + + WebGPUFlushContext.UploadTextureFromRegion( + lease.Api, + (Queue*)capability!.Queue, + (Texture*)capability.TargetTexture, + uploadRegion, + configuration.MemoryAllocator, + destX, + destY, + 0); + } + + /// + /// CPU fallback for layer compositing when the GPU path is unavailable but the + /// destination is a native GPU surface. + /// + private void ComposeLayerFallback( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + _ = destination.TryGetNativeSurface(out NativeSurface? destSurface); + _ = destSurface!.TryGetCapability(out WebGPUSurfaceCapability? destCapability); + + // Read destination and source from the GPU into CPU images. + if (!this.TryReadRegion(configuration, destination, destination.Bounds, out Image? destImage)) { - Buffer2DRegion uploadRegion = stagingRegion.GetSubRegion(uploadBounds); - WebGPUFlushContext.UploadTextureFromRegion( - uploadContext.Api, - uploadContext.Queue, - uploadContext.TargetTexture, - uploadRegion, - configuration.MemoryAllocator, - (uint)uploadBounds.X, - (uint)uploadBounds.Y, - 0); + return; } - else + + if (!this.TryReadRegion(configuration, source, source.Bounds, out Image? srcImage)) { + destImage.Dispose(); + return; + } + + using (destImage) + using (srcImage) + { + Buffer2DRegion destRegion = new(destImage.Frames.RootFrame.PixelBuffer); + ICanvasFrame destFrame = new MemoryCanvasFrame(destRegion); + ICanvasFrame srcFrame = new MemoryCanvasFrame(new Buffer2DRegion(srcImage.Frames.RootFrame.PixelBuffer)); + + this.fallbackBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options); + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); WebGPUFlushContext.UploadTextureFromRegion( - uploadContext.Api, - uploadContext.Queue, - uploadContext.TargetTexture, - stagingRegion, + lease.Api, + (Queue*)destCapability!.Queue, + (Texture*)destCapability.TargetTexture, + destRegion, configuration.MemoryAllocator); } } @@ -585,12 +723,6 @@ private bool TryRenderPreparedFlush( return false; } - if (flushContext.TargetTexture is null || flushContext.TargetView is null) - { - error = "WebGPU flush context does not expose required target resources."; - return false; - } - // Use the target texture directly as the backdrop source. // This avoids an extra texture allocation and target→source copy. TextureView* backdropTextureView = flushContext.TargetView; @@ -743,8 +875,8 @@ private bool TryRenderPreparedFlush( out WgpuBuffer* edgeBuffer, out nuint edgeBufferSize, out EdgePlacement[] edgePlacements, - out int totalEdgeCount, - out int totalBandOffsetEntries, + out _, + out _, out WgpuBuffer* bandOffsetsBuffer, out nuint bandOffsetsBufferSize, out StrokeExpandInfo strokeExpandInfo, @@ -791,26 +923,17 @@ private bool TryRenderPreparedFlush( return false; } - if (flushContext.RequiresReadback) - { - // CPU target: read back directly from the output texture at (0,0) - // instead of copying output→target and then reading from target. - flushContext.ReadbackSourceOverride = outputTexture; - } - else - { - // Native GPU surface: copy composited output back into the target. - CopyTextureRegion( - flushContext, - outputTexture, - 0, - 0, - flushContext.TargetTexture, - targetLocalBounds.X, - targetLocalBounds.Y, - targetLocalBounds.Width, - targetLocalBounds.Height); - } + // Copy composited output back into the target texture. + CopyTextureRegion( + flushContext, + outputTexture, + 0, + 0, + flushContext.TargetTexture, + targetLocalBounds.X, + targetLocalBounds.Y, + targetLocalBounds.Width, + targetLocalBounds.Height); error = null; return true; @@ -848,11 +971,8 @@ private bool TryDispatchPreparedCompositeCommands( return false; } - if (!CompositeComputeShader.TryGetInputSampleType(flushContext.TextureFormat, out TextureSampleType inputTextureSampleType)) - { - error = $"Prepared composite fine shader does not support texture format '{flushContext.TextureFormat}'."; - return false; - } + // TryGetCode already validates format support via TryGetInputSampleType internally. + _ = CompositeComputeShader.TryGetInputSampleType(flushContext.TextureFormat, out TextureSampleType inputTextureSampleType); string pipelineKey = $"prepared-composite-fine/{flushContext.TextureFormat}"; bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) @@ -1892,7 +2012,6 @@ private static bool TryCreateCompositionTexture( out TextureView* textureView, out string? error) { - texture = null; textureView = null; TextureDescriptor textureDescriptor = new() @@ -2031,20 +2150,11 @@ private static void PackPatternColors( } /// - /// Finalizes one flush by submitting command buffers and optionally reading results back to CPU memory. + /// Finalizes one flush by submitting command buffers. /// - private bool TryFinalizeFlush( - WebGPUFlushContext flushContext, - Buffer2DRegion cpuRegion, - Rectangle? readbackBounds) - where TPixel : unmanaged, IPixel + private static bool TryFinalizeFlush(WebGPUFlushContext flushContext) { flushContext.EndRenderPassIfOpen(); - if (flushContext.RequiresReadback) - { - return this.TryReadBackToCpuRegion(flushContext, cpuRegion, readbackBounds); - } - return TrySubmit(flushContext); } @@ -2086,179 +2196,7 @@ private static bool TrySubmit(WebGPUFlushContext flushContext) } /// - /// Copies target texture contents to the readback buffer and transfers bytes into destination CPU pixels. - /// - private bool TryReadBackToCpuRegion( - WebGPUFlushContext flushContext, - Buffer2DRegion destinationRegion, - Rectangle? readbackBounds) - where TPixel : unmanaged, IPixel - { - if (flushContext.TargetTexture is null || - flushContext.ReadbackBuffer is null || - flushContext.ReadbackByteCount == 0 || - flushContext.ReadbackBytesPerRow == 0) - { - return false; - } - - if (!flushContext.EnsureCommandEncoder()) - { - return false; - } - - Rectangle copyBounds = readbackBounds ?? new Rectangle(0, 0, destinationRegion.Width, destinationRegion.Height); - if (copyBounds.Width <= 0 || copyBounds.Height <= 0) - { - return true; - } - - uint copyBytesPerRow = checked((uint)copyBounds.Width * (uint)Unsafe.SizeOf()); - copyBytesPerRow = (copyBytesPerRow + 255U) & ~255U; - - // When ReadbackSourceOverride is set, the output texture already contains the - // composited result at (0,0), so we read from there instead of the target texture. - bool useOverride = flushContext.ReadbackSourceOverride is not null; - Texture* readbackTexture = useOverride ? flushContext.ReadbackSourceOverride : flushContext.TargetTexture; - uint readbackOriginX = useOverride ? 0 : (uint)copyBounds.X; - uint readbackOriginY = useOverride ? 0 : (uint)copyBounds.Y; - - ImageCopyTexture source = new() - { - Texture = readbackTexture, - MipLevel = 0, - Origin = new Origin3D(readbackOriginX, readbackOriginY, 0), - Aspect = TextureAspect.All - }; - - ImageCopyBuffer destination = new() - { - Buffer = flushContext.ReadbackBuffer, - Layout = new TextureDataLayout - { - Offset = 0, - BytesPerRow = copyBytesPerRow, - RowsPerImage = (uint)copyBounds.Height - } - }; - - Extent3D copySize = new((uint)copyBounds.Width, (uint)copyBounds.Height, 1); - flushContext.Api.CommandEncoderCopyTextureToBuffer(flushContext.CommandEncoder, in source, in destination, in copySize); - - if (!TrySubmit(flushContext)) - { - return false; - } - - return this.TryReadBackBufferToRegion( - flushContext, - flushContext.ReadbackBuffer, - checked((int)copyBytesPerRow), - destinationRegion, - copyBounds); - } - - /// - /// Maps the readback buffer and copies pixel data into the destination region. - /// - private bool TryReadBackBufferToRegion( - WebGPUFlushContext flushContext, - WgpuBuffer* readbackBuffer, - int sourceRowBytes, - Buffer2DRegion destinationRegion, - in Rectangle copyBounds) - where TPixel : unmanaged - { - int destinationRowBytes = checked(copyBounds.Width * Unsafe.SizeOf()); - int readbackByteCount = checked(sourceRowBytes * copyBounds.Height); - if (!this.TryMapReadBuffer(flushContext, readbackBuffer, (nuint)readbackByteCount, out byte* mappedData)) - { - return false; - } - - try - { - ReadOnlySpan sourceData = new(mappedData, readbackByteCount); - int destinationStrideBytes = checked(destinationRegion.Buffer.RowStride * Unsafe.SizeOf()); - - // Fast path for contiguous full-width rows. - if (copyBounds.X == 0 && - copyBounds.Width == destinationRegion.Width && - destinationRegion.Buffer.DangerousTryGetSingleMemory(out Memory contiguousDestination)) - { - Span destinationBytes = MemoryMarshal.AsBytes(contiguousDestination.Span); - int destinationStart = checked((destinationRegion.Rectangle.Y + copyBounds.Y) * destinationStrideBytes); - int copyByteCount = checked(destinationStrideBytes * copyBounds.Height); - Span destinationSlice = destinationBytes.Slice(destinationStart, copyByteCount); - if (sourceRowBytes == destinationStrideBytes) - { - sourceData[..copyByteCount].CopyTo(destinationSlice); - return true; - } - - for (int y = 0; y < copyBounds.Height; y++) - { - sourceData.Slice(y * sourceRowBytes, destinationStrideBytes) - .CopyTo(destinationSlice.Slice(y * destinationStrideBytes, destinationStrideBytes)); - } - - return true; - } - - for (int y = 0; y < copyBounds.Height; y++) - { - ReadOnlySpan sourceRow = sourceData.Slice(y * sourceRowBytes, destinationRowBytes); - MemoryMarshal.Cast(sourceRow).CopyTo( - destinationRegion.DangerousGetRowSpan(copyBounds.Y + y).Slice(copyBounds.X, copyBounds.Width)); - } - - return true; - } - finally - { - flushContext.Api.BufferUnmap(readbackBuffer); - } - } - - /// - /// Maps a readback buffer for CPU access and returns the mapped pointer. - /// - private bool TryMapReadBuffer( - WebGPUFlushContext flushContext, - WgpuBuffer* readbackBuffer, - nuint byteCount, - out byte* mappedData) - { - mappedData = null; - BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; - using ManualResetEventSlim callbackReady = new(false); - void Callback(BufferMapAsyncStatus status, void* userData) - { - mapStatus = status; - callbackReady.Set(); - } - - using PfnBufferMapCallback callbackPtr = PfnBufferMapCallback.From(Callback); - flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, byteCount, callbackPtr, null); - - if (!WaitForSignal(flushContext, callbackReady) || mapStatus != BufferMapAsyncStatus.Success) - { - return false; - } - - void* mapped = flushContext.Api.BufferGetConstMappedRange(readbackBuffer, 0, byteCount); - if (mapped is null) - { - flushContext.Api.BufferUnmap(readbackBuffer); - return false; - } - - mappedData = (byte*)mapped; - return true; - } - - /// - /// Releases all cached shared WebGPU resources and fallback staging resources. + /// Releases all cached shared WebGPU resources. /// public void Dispose() { @@ -2269,7 +2207,6 @@ public void Dispose() this.DisposeCoverageResources(); WebGPUFlushContext.ClearDeviceStateCache(); - WebGPUFlushContext.ClearFallbackStagingCache(); this.TestingLiveCoverageCount = 0; this.TestingIsGPUReady = false; @@ -2284,20 +2221,6 @@ public void Dispose() private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); - /// - /// Waits for a GPU callback signal, polling the device when the WGPU extension is available. - /// - private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEventSlim signal) - { - Wgpu? extension = flushContext.RuntimeLease.WgpuExtension; - if (extension is not null) - { - _ = extension.DevicePoll(flushContext.Device, true, (WrappedSubmissionIndex*)null); - } - - return signal.Wait(CallbackTimeoutMilliseconds); - } - /// /// Key that identifies a coverage definition for reuse within a flush. /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index 7ed81ccef..c2b0433e5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -28,13 +28,10 @@ internal enum CompositePipelineBlendMode /// internal sealed unsafe class WebGPUFlushContext : IDisposable { - private static readonly ConcurrentDictionary FallbackStagingCache = new(); private static readonly ConcurrentDictionary DeviceStateCache = new(); private bool disposed; private bool ownsTargetTexture; private bool ownsTargetView; - private bool ownsReadbackBuffer; - private DeviceSharedState.CpuTargetLease? cpuTargetLease; private readonly List transientBindGroups = []; private readonly List transientBuffers = []; private readonly List transientTextureViews = []; @@ -114,33 +111,6 @@ private WebGPUFlushContext( /// public TextureView* TargetView { get; private set; } - /// - /// Gets a value indicating whether CPU readback is required after GPU execution. - /// - public bool RequiresReadback { get; private set; } - - /// - /// Gets or sets an optional override texture to read back from instead of . - /// When set, readback copies from this texture at origin (0,0) rather than from the target - /// at composition bounds, eliminating an intermediate texture-to-texture copy. - /// - public Texture* ReadbackSourceOverride { get; set; } - - /// - /// Gets the readback buffer used when CPU readback is required. - /// - public WgpuBuffer* ReadbackBuffer { get; private set; } - - /// - /// Gets the readback row stride in bytes. - /// - public uint ReadbackBytesPerRow { get; private set; } - - /// - /// Gets the readback buffer byte size. - /// - public ulong ReadbackByteCount { get; private set; } - /// /// Gets the shared instance-data buffer used for parameter uploads. /// @@ -167,7 +137,9 @@ private WebGPUFlushContext( public RenderPassEncoder* PassEncoder { get; private set; } /// - /// Creates a flush context for either a native WebGPU surface or a CPU-backed frame. + /// Creates a flush context for a native WebGPU surface. + /// Returns when the frame does not expose a native surface + /// or the device lacks the required feature. /// /// The target frame. /// The expected GPU texture format. @@ -175,60 +147,29 @@ private WebGPUFlushContext( /// A device feature required by the pixel type for storage binding, or /// when no special feature is needed. /// - /// The unmanaged pixel size in bytes. /// The memory allocator for staging buffers. - /// Optional initial upload region for CPU targets. - /// The flush context, or if the device lacks . + /// The flush context, or when GPU execution is unavailable. public static WebGPUFlushContext? Create( ICanvasFrame frame, TextureFormat expectedTextureFormat, FeatureName requiredFeature, - int pixelSizeInBytes, - MemoryAllocator memoryAllocator, - Rectangle? initialUploadBounds = null) + MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { WebGPUSurfaceCapability? nativeCapability = TryGetNativeSurfaceCapability(frame, expectedTextureFormat); + if (nativeCapability is null) + { + return null; + } + WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); try { - Device* device; - Queue* queue; - TextureFormat textureFormat; - Rectangle bounds = frame.Bounds; - DeviceSharedState deviceState; - WebGPUFlushContext context; - - if (nativeCapability is not null) - { - device = (Device*)nativeCapability.Device; - queue = (Queue*)nativeCapability.Queue; - textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); - bounds = new Rectangle(0, 0, nativeCapability.Width, nativeCapability.Height); - deviceState = GetOrCreateDeviceState(lease.Api, device); - - if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) - { - lease.Dispose(); - return null; - } - - context = new WebGPUFlushContext(lease, device, queue, in bounds, textureFormat, memoryAllocator, deviceState); - context.InitializeNativeTarget(nativeCapability); - return context; - } - - if (!frame.TryGetCpuRegion(out Buffer2DRegion cpuRegion)) - { - throw new NotSupportedException("Frame does not expose a GPU-native surface or CPU region."); - } - - if (!WebGPURuntime.TryGetOrCreateDevice(out device, out queue, out string? error)) - { - throw new InvalidOperationException(error ?? "WebGPU device auto-provisioning failed."); - } - - deviceState = GetOrCreateDeviceState(lease.Api, device); + Device* device = (Device*)nativeCapability.Device; + Queue* queue = (Queue*)nativeCapability.Queue; + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); + Rectangle bounds = new(0, 0, nativeCapability.Width, nativeCapability.Height); + DeviceSharedState deviceState = GetOrCreateDeviceState(lease.Api, device); if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) { @@ -236,43 +177,7 @@ private WebGPUFlushContext( return null; } - context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, memoryAllocator, deviceState); - nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(frame); - context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, targetIdentity, initialUploadBounds); - return context; - } - catch - { - lease.Dispose(); - throw; - } - } - - /// - /// Creates a flush context intended for fallback upload into a writable native surface. - /// - public static WebGPUFlushContext CreateUploadContext(ICanvasFrame frame, MemoryAllocator memoryAllocator) - where TPixel : unmanaged, IPixel - { - WebGPUSurfaceCapability? nativeCapability = - TryGetWritableNativeSurfaceCapability(frame) - ?? throw new NotSupportedException("Fallback upload requires a native WebGPU surface exposing writable device, queue, and texture handles."); - - WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); - try - { - Rectangle bounds = new(0, 0, nativeCapability.Width, nativeCapability.Height); - TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); - Device* device = (Device*)nativeCapability.Device; - DeviceSharedState deviceState = GetOrCreateDeviceState(lease.Api, device); - WebGPUFlushContext context = new( - lease, - device, - (Queue*)nativeCapability.Queue, - in bounds, - textureFormat, - memoryAllocator, - deviceState); + WebGPUFlushContext context = new(lease, device, queue, in bounds, textureFormat, memoryAllocator, deviceState); context.InitializeNativeTarget(nativeCapability); return context; } @@ -283,44 +188,6 @@ public static WebGPUFlushContext CreateUploadContext(ICanvasFrame - /// Rents a CPU fallback staging buffer for the specified pixel type and bounds. - /// - public static FallbackStagingLease RentFallbackStaging(MemoryAllocator allocator, in Rectangle bounds) - where TPixel : unmanaged, IPixel - { - IDisposable entry = FallbackStagingCache.GetOrAdd( - typeof(TPixel), - static _ => new FallbackStagingEntry()); - - return ((FallbackStagingEntry)entry).Rent(allocator, in bounds); - } - - /// - /// Clears all cached CPU fallback staging buffers. - /// - public static void ClearFallbackStagingCache() - { - foreach (IDisposable entry in FallbackStagingCache.Values) - { - entry.Dispose(); - } - - FallbackStagingCache.Clear(); - } - - /// - /// Releases all cached CPU target resources associated with the specified target identity. - /// - /// The target frame identity whose cached resources should be released. - public static void ReleaseCpuTargetEntries(nint targetIdentity) - { - foreach (DeviceSharedState state in DeviceStateCache.Values) - { - state.ReleaseCpuTargetEntries(targetIdentity); - } - } - /// /// Clears all cached device-scoped shared state. /// @@ -540,14 +407,6 @@ public void Dispose() this.InstanceBufferWriteOffset = 0; - this.cpuTargetLease?.Dispose(); - this.cpuTargetLease = null; - - if (this.ownsReadbackBuffer && this.ReadbackBuffer is not null) - { - this.Api.BufferRelease(this.ReadbackBuffer); - } - if (this.ownsTargetView && this.TargetView is not null) { this.Api.TextureViewRelease(this.TargetView); @@ -586,13 +445,8 @@ public void Dispose() // Cache entries point to transient texture views that are released above. this.cachedSourceTextureViews.Clear(); - this.ReadbackBuffer = null; this.TargetView = null; this.TargetTexture = null; - this.ReadbackBytesPerRow = 0; - this.ReadbackByteCount = 0; - this.RequiresReadback = false; - this.ownsReadbackBuffer = false; this.ownsTargetView = false; this.ownsTargetTexture = false; @@ -622,66 +476,8 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability) { this.TargetTexture = (Texture*)capability.TargetTexture; this.TargetView = (TextureView*)capability.TargetTextureView; - this.RequiresReadback = false; - this.ReadbackBuffer = null; - this.ReadbackBytesPerRow = 0; - this.ReadbackByteCount = 0; - this.ownsTargetTexture = false; - this.ownsTargetView = false; - this.ownsReadbackBuffer = false; - } - - private void InitializeCpuTarget( - Buffer2DRegion cpuRegion, - int pixelSizeInBytes, - nint targetIdentity, - Rectangle? initialUploadBounds) - where TPixel : unmanaged - { - int width = cpuRegion.Width; - int height = cpuRegion.Height; - DeviceSharedState.CpuTargetLease lease = this.DeviceState.RentCpuTarget( - this.TextureFormat, - width, - height, - pixelSizeInBytes, - targetIdentity); - Texture* targetTexture = lease.TargetTexture; - TextureView* targetView = lease.TargetView; - WgpuBuffer* readbackBuffer = lease.ReadbackBuffer; - uint readbackRowBytes = lease.ReadbackBytesPerRow; - ulong readbackByteCount = lease.ReadbackByteCount; - - try - { - if (initialUploadBounds is Rectangle uploadBounds && - uploadBounds.Width > 0 && - uploadBounds.Height > 0) - { - Buffer2DRegion uploadRegion = cpuRegion.GetSubRegion(uploadBounds); - UploadTextureFromRegion(this.Api, this.Queue, targetTexture, uploadRegion, this.MemoryAllocator, (uint)uploadBounds.X, (uint)uploadBounds.Y, 0); - } - else - { - UploadTextureFromRegion(this.Api, this.Queue, targetTexture, cpuRegion, this.MemoryAllocator); - } - } - catch - { - lease.Dispose(); - throw; - } - - this.cpuTargetLease = lease; - this.TargetTexture = targetTexture; - this.TargetView = targetView; - this.ReadbackBuffer = readbackBuffer; - this.ReadbackBytesPerRow = readbackRowBytes; - this.ReadbackByteCount = readbackByteCount; - this.RequiresReadback = true; this.ownsTargetTexture = false; this.ownsTargetView = false; - this.ownsReadbackBuffer = false; } private static WebGPUSurfaceCapability? TryGetNativeSurfaceCapability(ICanvasFrame frame, TextureFormat expectedTextureFormat) @@ -713,32 +509,6 @@ private void InitializeCpuTarget( return capability; } - private static WebGPUSurfaceCapability? TryGetWritableNativeSurfaceCapability(ICanvasFrame frame) - where TPixel : unmanaged, IPixel - { - if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface) || - !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability)) - { - return null; - } - - if (capability.Device == 0 || capability.Queue == 0 || capability.TargetTexture == 0) - { - return null; - } - - Rectangle bounds = frame.Bounds; - if (bounds.X < 0 || - bounds.Y < 0 || - bounds.Right > capability.Width || - bounds.Bottom > capability.Height) - { - return null; - } - - return capability; - } - internal static void UploadTextureFromRegion( WebGPU api, Queue* queue, @@ -834,7 +604,6 @@ internal static void UploadTextureFromRegion( /// internal sealed class DeviceSharedState : IDisposable { - private readonly ConcurrentDictionary cpuTargetCache = new(); private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); private readonly ConcurrentDictionary sharedBuffers = new(StringComparer.Ordinal); @@ -902,43 +671,6 @@ private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* return result; } - /// - /// Rents CPU-target staging resources for a destination texture shape and format. - /// - /// The destination texture format. - /// The destination width. - /// The destination height. - /// The destination pixel size in bytes. - /// Identity of the target frame to prevent cache collisions. - /// A lease for staging resources. - public CpuTargetLease RentCpuTarget( - TextureFormat textureFormat, - int width, - int height, - int pixelSizeInBytes, - nint targetIdentity) - { - CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes, targetIdentity); - CpuTargetEntry entry = this.cpuTargetCache.GetOrAdd(key, static _ => new CpuTargetEntry()); - return entry.Rent(this.Api, this.Device, in key); - } - - /// - /// Releases and removes all CPU target cache entries matching the specified target identity. - /// - /// The target frame identity whose entries should be released. - public void ReleaseCpuTargetEntries(nint targetIdentity) - { - foreach (KeyValuePair pair in this.cpuTargetCache) - { - if (pair.Key.TargetIdentity == targetIdentity && - this.cpuTargetCache.TryRemove(pair.Key, out CpuTargetEntry? entry)) - { - entry.Dispose(this.Api); - } - } - } - /// /// Gets or creates a graphics pipeline used for composite rendering. /// @@ -1224,12 +956,6 @@ public void Dispose() this.sharedBuffers.Clear(); - foreach (CpuTargetEntry entry in this.cpuTargetCache.Values) - { - entry.Dispose(this.Api); - } - - this.cpuTargetCache.Clear(); this.disposed = true; } @@ -1464,358 +1190,6 @@ private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfra } } - /// - /// Cache key for CPU-target staging resources. - /// - internal readonly struct CpuTargetCacheKey( - TextureFormat textureFormat, - int width, - int height, - int pixelSizeInBytes, - nint targetIdentity) : IEquatable - { - /// - /// Gets the texture format for the cached CPU target. - /// - public TextureFormat TextureFormat { get; } = textureFormat; - - /// - /// Gets the target width. - /// - public int Width { get; } = width; - - /// - /// Gets the target height. - /// - public int Height { get; } = height; - - /// - /// Gets the pixel size in bytes. - /// - public int PixelSizeInBytes { get; } = pixelSizeInBytes; - - /// - /// Gets the identity of the target frame to prevent different targets - /// with the same dimensions from sharing GPU texture content. - /// - public nint TargetIdentity { get; } = targetIdentity; - - /// - /// Determines whether this key equals another CPU target cache key. - /// - /// The key to compare. - /// if all fields match; otherwise . - public bool Equals(CpuTargetCacheKey other) - => this.TextureFormat == other.TextureFormat && - this.Width == other.Width && - this.Height == other.Height && - this.PixelSizeInBytes == other.PixelSizeInBytes && - this.TargetIdentity == other.TargetIdentity; - - /// - public override bool Equals(object? obj) => obj is CpuTargetCacheKey other && this.Equals(other); - - /// - public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes, this.TargetIdentity); - } - - /// - /// Cache entry that owns the CPU-target staging resources. - /// - internal sealed class CpuTargetEntry - { - private Texture* targetTexture; - private TextureView* targetView; - private WgpuBuffer* readbackBuffer; - private uint readbackBytesPerRow; - private ulong readbackByteCount; - private int inUse; - - /// - /// Rents staging resources for the specified cache key. - /// - internal CpuTargetLease Rent(WebGPU api, Device* device, in CpuTargetCacheKey key) - { - if (Interlocked.CompareExchange(ref this.inUse, 1, 0) == 0) - { - try - { - this.EnsureResources(api, device, in key); - } - catch - { - this.Release(); - throw; - } - - return new CpuTargetLease( - api, - this, - ownsResources: false, - this.targetTexture, - this.targetView, - this.readbackBuffer, - this.readbackBytesPerRow, - this.readbackByteCount); - } - - if (!TryCreateCpuTargetResources( - api, - device, - in key, - out Texture* temporaryTexture, - out TextureView* temporaryView, - out WgpuBuffer* temporaryReadbackBuffer, - out uint temporaryReadbackRowBytes, - out ulong temporaryReadbackByteCount)) - { - throw new InvalidOperationException("Failed to create temporary CPU flush target resources."); - } - - return new CpuTargetLease( - api, - owner: null, - ownsResources: true, - temporaryTexture, - temporaryView, - temporaryReadbackBuffer, - temporaryReadbackRowBytes, - temporaryReadbackByteCount); - } - - /// - /// Marks this entry as available for reuse. - /// - internal void Release() => Volatile.Write(ref this.inUse, 0); - - /// - /// Releases all resources currently owned by this entry. - /// - internal void Dispose(WebGPU api) - { - ReleaseCpuTargetResources(api, this.targetTexture, this.targetView, this.readbackBuffer); - this.targetTexture = null; - this.targetView = null; - this.readbackBuffer = null; - this.readbackBytesPerRow = 0; - this.readbackByteCount = 0; - this.inUse = 0; - } - - private void EnsureResources(WebGPU api, Device* device, in CpuTargetCacheKey key) - { - if (this.targetTexture is not null && - this.targetView is not null && - this.readbackBuffer is not null) - { - return; - } - - ReleaseCpuTargetResources(api, this.targetTexture, this.targetView, this.readbackBuffer); - this.targetTexture = null; - this.targetView = null; - this.readbackBuffer = null; - this.readbackBytesPerRow = 0; - this.readbackByteCount = 0; - - if (!TryCreateCpuTargetResources( - api, - device, - in key, - out this.targetTexture, - out this.targetView, - out this.readbackBuffer, - out this.readbackBytesPerRow, - out this.readbackByteCount)) - { - throw new InvalidOperationException("Failed to create cached CPU flush target resources."); - } - } - - private static bool TryCreateCpuTargetResources( - WebGPU api, - Device* device, - in CpuTargetCacheKey key, - out Texture* targetTexture, - out TextureView* targetView, - out WgpuBuffer* readbackBuffer, - out uint readbackBytesPerRow, - out ulong readbackByteCount) - { - targetTexture = null; - targetView = null; - readbackBuffer = null; - readbackBytesPerRow = 0; - readbackByteCount = 0; - - uint textureRowBytes = checked((uint)key.Width * (uint)key.PixelSizeInBytes); - readbackBytesPerRow = AlignTo256(textureRowBytes); - readbackByteCount = checked((ulong)readbackBytesPerRow * (uint)key.Height); - - TextureDescriptor targetTextureDescriptor = new() - { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)key.Width, (uint)key.Height, 1), - Format = key.TextureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - targetTexture = api.DeviceCreateTexture(device, in targetTextureDescriptor); - if (targetTexture is null) - { - return false; - } - - TextureViewDescriptor targetViewDescriptor = new() - { - Format = key.TextureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - targetView = api.TextureCreateView(targetTexture, in targetViewDescriptor); - if (targetView is null) - { - api.TextureRelease(targetTexture); - targetTexture = null; - return false; - } - - BufferDescriptor readbackDescriptor = new() - { - Usage = BufferUsage.MapRead | BufferUsage.CopyDst, - Size = readbackByteCount - }; - - readbackBuffer = api.DeviceCreateBuffer(device, in readbackDescriptor); - if (readbackBuffer is null) - { - api.TextureViewRelease(targetView); - api.TextureRelease(targetTexture); - targetView = null; - targetTexture = null; - return false; - } - - return true; - } - - private static void ReleaseCpuTargetResources( - WebGPU api, - Texture* targetTexture, - TextureView* targetView, - WgpuBuffer* readbackBuffer) - { - if (readbackBuffer is not null) - { - api.BufferRelease(readbackBuffer); - } - - if (targetView is not null) - { - api.TextureViewRelease(targetView); - } - - if (targetTexture is not null) - { - api.TextureRelease(targetTexture); - } - } - } - - /// - /// Lease wrapper for CPU-target staging resources. - /// - public sealed class CpuTargetLease : IDisposable - { - private readonly WebGPU api; - private readonly CpuTargetEntry? owner; - private readonly bool ownsResources; - private int disposed; - - internal CpuTargetLease( - WebGPU api, - CpuTargetEntry? owner, - bool ownsResources, - Texture* targetTexture, - TextureView* targetView, - WgpuBuffer* readbackBuffer, - uint readbackBytesPerRow, - ulong readbackByteCount) - { - this.api = api; - this.owner = owner; - this.ownsResources = ownsResources; - this.TargetTexture = targetTexture; - this.TargetView = targetView; - this.ReadbackBuffer = readbackBuffer; - this.ReadbackBytesPerRow = readbackBytesPerRow; - this.ReadbackByteCount = readbackByteCount; - } - - /// - /// Gets the target texture used for CPU staging operations. - /// - public Texture* TargetTexture { get; } - - /// - /// Gets the texture view of . - /// - public TextureView* TargetView { get; } - - /// - /// Gets the readback buffer used to copy staged pixels to CPU memory. - /// - public WgpuBuffer* ReadbackBuffer { get; } - - /// - /// Gets the readback row stride in bytes. - /// - public uint ReadbackBytesPerRow { get; } - - /// - /// Gets the total readback buffer size in bytes. - /// - public ulong ReadbackByteCount { get; } - - /// - /// Releases leased resources or returns ownership to the shared cache entry. - /// - public void Dispose() - { - if (Interlocked.Exchange(ref this.disposed, 1) != 0) - { - return; - } - - if (this.ownsResources) - { - if (this.ReadbackBuffer is not null) - { - this.api.BufferRelease(this.ReadbackBuffer); - } - - if (this.TargetView is not null) - { - this.api.TextureViewRelease(this.TargetView); - } - - if (this.TargetTexture is not null) - { - this.api.TextureRelease(this.TargetTexture); - } - } - - this.owner?.Release(); - } - } - /// /// Shared render-pipeline infrastructure for compositing variants. /// @@ -1850,115 +1224,4 @@ private sealed class SharedBufferInfrastructure public BufferUsage Usage { get; set; } } } - - /// - /// Lease over a CPU fallback staging region. - /// - /// The pixel type of the staging region. - public sealed class FallbackStagingLease : IDisposable - where TPixel : unmanaged, IPixel - { - private readonly FallbackStagingEntry? owner; - private readonly Buffer2D? temporaryBuffer; - private int disposed; - - /// - /// Initializes a new instance of the class. - /// - internal FallbackStagingLease( - Buffer2DRegion region, - FallbackStagingEntry? owner, - Buffer2D? temporaryBuffer) - { - this.Region = region; - this.owner = owner; - this.temporaryBuffer = temporaryBuffer; - } - - /// - /// Gets the staging region for fallback rendering. - /// - public Buffer2DRegion Region { get; } - - /// - public void Dispose() - { - if (Interlocked.Exchange(ref this.disposed, 1) != 0) - { - return; - } - - this.temporaryBuffer?.Dispose(); - this.owner?.Release(); - } - } - - /// - /// Cached staging entry for one pixel type. - /// - /// The pixel type stored by this entry. - internal sealed class FallbackStagingEntry : IDisposable - where TPixel : unmanaged, IPixel - { - private Buffer2D? buffer; - private Size size; - private int inUse; - - /// - /// Rents a staging lease for the specified bounds. - /// - public FallbackStagingLease Rent(MemoryAllocator allocator, in Rectangle bounds) - { - if (Interlocked.CompareExchange(ref this.inUse, 1, 0) == 0) - { - this.EnsureSize(allocator, bounds.Size); - Buffer2D? current = this.buffer; - if (current is null) - { - this.Release(); - throw new InvalidOperationException("Fallback staging buffer is not initialized."); - } - - return new FallbackStagingLease( - new Buffer2DRegion(current, bounds), - this, - temporaryBuffer: null); - } - - Buffer2D temporary = allocator.Allocate2D(bounds.Size, AllocationOptions.Clean); - return new FallbackStagingLease( - new Buffer2DRegion(temporary, bounds), - owner: null, - temporaryBuffer: temporary); - } - - /// - /// Releases an acquired cached staging entry. - /// - public void Release() - => Volatile.Write(ref this.inUse, 0); - - /// - public void Dispose() - { - this.buffer?.Dispose(); - this.buffer = null; - this.size = default; - this.inUse = 0; - } - - private void EnsureSize(MemoryAllocator allocator, Size requiredSize) - { - if (this.buffer is not null && - this.size.Width >= requiredSize.Width && - this.size.Height >= requiredSize.Height) - { - return; - } - - this.buffer?.Dispose(); - this.buffer = allocator.Allocate2D(requiredSize, AllocationOptions.Clean); - this.size = requiredSize; - } - } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md new file mode 100644 index 000000000..ae7266fe3 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md @@ -0,0 +1,249 @@ +# DefaultDrawingBackend + +This document describes the CPU-based rasterization and composition pipeline implemented by `DefaultDrawingBackend`. + +## Overview + +`DefaultDrawingBackend` is a singleton (`DefaultDrawingBackend.Instance`) implementing `IDrawingBackend`. It performs all path rasterization, brush application, and pixel compositing on the CPU using fixed-point scanline math with band-based parallelism. + +## End-to-End Flow + +```text +DrawingCanvasBatcher.FlushCompositions() + -> IDrawingBackend.FlushCompositions(configuration, target, scene) + -> target.TryGetCpuRegion(out region) + -> CompositionScenePlanner.CreatePreparedBatches(commands, targetBounds) + -> clip each command to target bounds + -> group contiguous commands by DefinitionKey + -> keep prepared destination/source offsets + -> for each CompositionBatch: + -> FlushPreparedBatch(configuration, region, batch) + -> create BrushApplicator[] for all commands in batch + -> create RowOperation (scanline callback) + -> if batch.Definition.IsStroke: + -> DefaultRasterizer.RasterizeStrokeRows(definition, rowHandler, allocator) + -> else: + -> DefaultRasterizer.RasterizeRows(definition, rowHandler, allocator) + -> dispose applicators +``` + +## Scene Planning (CompositionScenePlanner) + +`CompositionScenePlanner.CreatePreparedBatches()` transforms raw `CompositionCommand` lists into `CompositionBatch` groups: + +1. **Clip** each command's destination to target bounds; discard commands with zero-area overlap. +2. **Group** contiguous commands sharing the same `DefinitionKey` (path identity + rasterizer options hash) into a single batch. Commands with different definitions break the batch. +3. **Prepare** each command with clipped `DestinationRegion` and `SourceOffset` mapping rasterized coverage to the clipped region. + +For stroke batches, after dash expansion grows the interest rect, `ReprepareBatchCommands()` re-clips all commands to the updated bounds. + +## DefaultRasterizer + +The rasterizer is a 3300-line fixed-point scanline engine in `DefaultRasterizer.cs`. + +### Fixed-Point Representation + +``` +Format: 24.8 (8 fractional bits) +One: 256 +Sub-pixels: 256 steps per pixel +Area scale: 512 (area-to-coverage shift = 9) +``` + +### Edge Table Construction + +Input paths are flattened to line segments. Each segment is converted to fixed-point `EdgeData` (x0, y0, x1, y1) with: +- Vertical Liang-Barsky clipping to the band bounds +- Horizontal edges filtered out (fy0 == fy1) +- Y-ordering enforced (swap if y0 > y1) + +### Execution Modes + +```text +IF tileCount == 1 OR edgeCount <= 64 + -> RasterizeSingleTileDirect (no parallelism overhead) +ELSE IF ProcessorCount >= 2 + -> Parallel.For across tiles (band-sorted edges) +ELSE + -> Sequential band loop with reusable scratch +``` + +**Parallel:** `MaxDegreeOfParallelism = min(12, ProcessorCount, tileCount)`. Each worker gets an isolated `WorkerScratch` instance. No shared mutable state during tile processing. + +**Sequential:** Single reusable `WorkerScratch`, band loop with context reset between bands. + +### Band-Based Processing + +Tile height: 16 pixels. Each band processes only edges that intersect its Y range. Edges are duplicated across all touched bands during band-sort. + +### Per-Band Scan Conversion (Context) + +The `Context` ref struct holds per-band working memory: + +| Buffer | Purpose | +|---|---| +| `bitVectors[]` | Sparse touched-column tracking (nuint[] per row) | +| `coverArea[]` | Signed area accumulation (2 ints per pixel: delta + area) | +| `startCover[]` | Carry cover from edges left of the band's X origin | +| `rowMinTouchedColumn[]` | Left bound per row for sparse iteration | +| `rowMaxTouchedColumn[]` | Right bound per row for sparse iteration | +| `rowHasBits[]`, `rowTouched[]` | Touch flags for sparse reset | +| `touchedRows[]` | Touched row indices for cleanup | +| `scanline[]` | Output coverage buffer (float per pixel) | + +### Edge Rasterization + +Bresenham-style line algorithm. For each cell an edge crosses: +- Register signed delta at cell entry X +- Register -delta at cell exit X +- Update area accumulation based on cell-fraction coverage +- Track touched rows and columns via bit vectors + +### Row Coverage Emission + +For each touched row: +1. Iterate only set bits in the touched-column bit vector (sparse). +2. Accumulate winding number from `startCover` + running `coverArea` deltas. +3. Apply fill rule: + - **NonZero:** Clamp |winding| to [0, 1] + - **EvenOdd:** `(winding & mask) > CoverageStepCount ? 0 : 1` +4. Apply antialiasing mode: + - **Antialiased:** Coverage = area / 512, clamped to [0, 1] + - **Aliased:** Coverage > threshold → 1.0, else 0.0 +5. Coalesce consecutive cells with equal coverage into spans. +6. Emit span via `RasterizerCoverageRowHandler` callback. + +### Memory Budget + +``` +Band memory budget: 64 MB +Bytes per row = (wordsPerRow × pointer_size) + (coverStride × 4) + 4 +Max band rows = min(height, 64MB / bytesPerRow) +``` + +### Sparse Reset + +After emitting all rows in a band, only touched rows are cleared—not the full scratch buffer. This avoids full-buffer clears when geometry is sparse relative to the interest rectangle. + +## Stroke Processing + +### Stroke Expansion + +For each stroke definition, the rasterizer performs per-band parallel expansion: + +1. **Centerline collection:** Flatten path into contours with open/closed tracking. +2. **Dash splitting (optional):** If the definition has a dash pattern, `SplitPathExtensions.GenerateDashes()` segments the centerline into open dash sub-paths. +3. **Stroke edge descriptors:** Each contour segment produces: + - **Side edges** (Flags=None): Left/right offset by halfWidth along the perpendicular + - **Join edges** (Flags=Join): Vertex + adjacent vertex for miter/round/bevel computation + - **Cap edges** (CapStart/CapEnd): Endpoint + direction for butt/square/round caps + +### Join Expansion + +| Join Type | Algorithm | +|---|---| +| Miter | Intersection of offset lines; revert to bevel if miterDist > miterLimit × halfWidth | +| MiterRound | Blend bevel→miter at limit distance | +| Round | Circular arc subdivided at ~0.5 × halfWidth step density | +| Bevel | Straight diagonal between offset endpoints | + +### Cap Expansion + +| Cap Type | Algorithm | +|---|---| +| Butt | Single perpendicular edge at endpoint | +| Square | Rectangle extending halfWidth beyond endpoint | +| Round | Semicircular arc subdivided at ~0.5 × halfWidth | + +### Band Sorting + +`TryBuildBandSortedStrokeEdges()` duplicates stroke edges into all touched bands. Expansion Y bounds include halfWidth × max(miterLimit, 1). Each band expands and rasterizes independently. + +## Brush Application + +After rasterization emits a coverage scanline, `RowOperation.InvokeCoverageRow()` applies brushes: + +```text +for each command overlapping this scanline row: + -> clamp coverage region to command DestinationRegion + -> compute destination pixel coordinates + -> BrushApplicator[i].Apply(coverage.Slice(...), destX, destY) +``` + +Each `BrushApplicator` (abstract base in `BrushApplicator.cs`): +1. Samples brush color at destination coordinates +2. Multiplies by coverage +3. Composites into destination via `PixelBlender` + +Concrete applicator types: Solid, LinearGradient, RadialGradient, EllipticGradient, SweepGradient, Pattern, Image, Recolor. + +## ComposeLayer (CPU Path) + +```text +IDrawingBackend.ComposeLayer(source, destination, offset, options) + -> extract CPU regions from source and destination frames + -> clamp compositing region to both bounds + -> allocate float[] amounts filled with BlendPercentage + -> for each row y in intersection: + -> srcRow = sourceRegion[y].Slice(startX, width) + -> dstRow = destRegion[dstY].Slice(dstX, width) + -> PixelBlender.Blend(config, dstRow, dstRow, srcRow, amounts) +``` + +The `PixelBlender` implements the full Porter-Duff alpha composition with configurable color blend mode and per-pixel blend percentage (layer opacity). + +## TryReadRegion + +```text +IDrawingBackend.TryReadRegion(target, sourceRect, out image) + -> target.TryGetCpuRegion(out region) + -> create Image from region sub-rectangle + -> copy pixel rows +``` + +Used by `DrawingCanvas.Process()` to read back pixels for image processing operations. + +## Composition Pipeline + +The full pixel composition formula (generalized Porter-Duff): + +``` +Cₒᵤₜ = αₛ × BlendMode(Cₛ, Cₐ) + αₐ × Cₐ × (1 - αₛ) +αₒᵤₜ = αₛ + αₐ × (1 - αₛ) +``` + +Where: +- αₛ = source alpha (brush alpha × coverage × blend percentage) +- αₐ = destination alpha +- Cₛ = source color (from brush) +- Cₐ = destination color (backdrop) +- BlendMode = color blend operation (Normal, Multiply, Screen, Overlay, etc.) + +## Threading Model + +| Condition | Path | +|---|---| +| 1 tile OR ≤64 edges | Single-tile direct (no overhead) | +| ProcessorCount ≥ 2, multiple tiles | Parallel.For, MaxDOP = min(12, cores, tiles) | +| Single core | Sequential band loop | + +Worker isolation: Each parallel worker owns its own `WorkerScratch`. No synchronization during tile processing. Coverage emission callbacks are inherently thread-safe because each tile covers disjoint pixel rows. + +## Key Data Structures + +| Type | Purpose | +|---|---| +| `CompositionCommand` | Normalized draw instruction (path + brush + options) | +| `CompositionBatch` | Commands grouped by shared coverage definition | +| `PreparedCompositionCommand` | Command clipped to target bounds with source offset | +| `CompositionCoverageDefinition` | Stable identity for coverage reuse (path + rasterizer state) | +| `RasterizerOptions` | Interest rect, fill rule, rasterization mode, sampling origin, antialias threshold | +| `WorkerScratch` | Per-worker rasterization memory (bit vectors, area buffers, scanline) | + +## Memory Management + +- `WorkerScratch` allocated via `MemoryAllocator` with `IMemoryOwner` for pool return. +- Sequential path reuses a single `WorkerScratch` across batches if dimensions are compatible. +- Parallel path creates fresh worker-local scratch per `Parallel.For` iteration (discarded after). +- `BrushApplicator` instances are disposed after each batch flush. +- 64 MB band memory budget caps per-band allocation. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 15b5eb31d..a61268a62 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Memory; @@ -71,13 +72,86 @@ public void FlushCompositions( } } + /// + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + if (!source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames."); + } + + if (!destination.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames."); + } + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options); + float blendPercentage = options.BlendPercentage; + + int srcWidth = sourceRegion.Width; + int srcHeight = sourceRegion.Height; + int dstWidth = destinationRegion.Width; + int dstHeight = destinationRegion.Height; + + // Clamp the compositing region to both source and destination bounds. + int startX = Math.Max(0, -destinationOffset.X); + int startY = Math.Max(0, -destinationOffset.Y); + int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X); + int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y); + + if (endX <= startX || endY <= startY) + { + return; + } + + int width = endX - startX; + + // Allocate a reusable per-row amount buffer from the memory pool. + using IMemoryOwner amountsOwner = configuration.MemoryAllocator.Allocate(width); + Span amounts = amountsOwner.Memory.Span; + amounts.Fill(blendPercentage); + + for (int y = startY; y < endY; y++) + { + Span srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width); + int dstX = destinationOffset.X + startX; + int dstY = destinationOffset.Y + y; + Span dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width); + + blender.Blend(configuration, dstRow, dstRow, srcRow, amounts); + } + } + + /// + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TPixel : unmanaged, IPixel + { + Buffer2D buffer = configuration.MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean); + return new MemoryCanvasFrame(new Buffer2DRegion(buffer)); + } + /// public void ReleaseFrameResources( Configuration configuration, ICanvasFrame target) where TPixel : unmanaged, IPixel { - // No cached resources to release for CPU-only backend. + if (target.TryGetCpuRegion(out Buffer2DRegion region)) + { + region.Buffer.Dispose(); + } } /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 53b2b0f15..1b0dbc6aa 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -46,6 +46,46 @@ public bool TryReadRegion( [NotNullWhen(true)] out Image? image) where TPixel : unmanaged, IPixel; + /// + /// Composites a layer surface onto a destination frame using the specified graphics options. + /// + /// The pixel format. + /// Active processing configuration. + /// The layer frame to composite. + /// The destination frame to composite onto. + /// + /// The offset in the destination where the layer's top-left corner is placed. + /// + /// + /// Graphics options controlling blend mode, alpha composition, and opacity. + /// + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel; + + /// + /// Creates a layer frame for use during SaveLayer. + /// The returned frame is initialized to transparent and must be disposed by the caller. + /// CPU backends return a frame backed by an ; + /// GPU backends may return a native frame backed by a GPU texture. + /// + /// The pixel format. + /// Active processing configuration. + /// The current target frame that the layer will composite onto. + /// Layer width in pixels. + /// Layer height in pixels. + /// A new disposable layer frame. + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TPixel : unmanaged, IPixel; + /// /// Releases any backend resources cached against the specified target frame. /// diff --git a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md new file mode 100644 index 000000000..779559940 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md @@ -0,0 +1,298 @@ +# DrawingCanvas + +This document describes the architecture, state management, and object lifecycle of `DrawingCanvas`. + +## Overview + +`DrawingCanvas` is the high-level drawing API. It manages a state stack, command batching, layer compositing, and delegates rasterization to an `IDrawingBackend`. It implements a deferred command model—draw calls queue `CompositionCommand` objects in a batcher, which are flushed to the backend on `Flush()` or `Dispose()`. + +## Class Structure + +```text +DrawingCanvas : IDrawingCanvas, IDisposable + Fields: + configuration : Configuration + backend : IDrawingBackend + targetFrame : ICanvasFrame (root frame, immutable) + batcher : DrawingCanvasBatcher (reassigned on SaveLayer/Restore) + savedStates : Stack (min depth 1) + layerDataStack : Stack> (one per active SaveLayer) + pendingImageResources : List> (temp images awaiting flush) + isRoot : bool (only root releases frame resources) + isDisposed : bool +``` + +## State Management + +### DrawingCanvasState (Immutable Snapshot) + +```text +DrawingCanvasState + Options : DrawingOptions (by reference, not deep-cloned) + ClipPaths : IReadOnlyList + IsLayer : bool (init-only, default false) + LayerOptions : GraphicsOptions? (init-only, set for layers) + LayerBounds : Rectangle? (init-only, set for layers) +``` + +### Save / Restore + +```text +Save() + -> push new DrawingCanvasState(current.Options, current.ClipPaths) + -> IsLayer = false (prevents spurious layer compositing on Restore) + -> return SaveCount + +Save(options, clipPaths) + -> Save(), then replace top state with new options/clips + -> return SaveCount + +Restore() + -> if SaveCount <= 1: no-op + -> pop top state + -> if state.IsLayer: CompositeAndPopLayer(state) + +RestoreTo(saveCount) + -> pop states until SaveCount == target + -> each layer state triggers CompositeAndPopLayer +``` + +`ResolveState()` returns `savedStates.Peek()`—every drawing operation reads the active state from here. + +## SaveLayer Lifecycle + +### Purpose + +SaveLayer enables group-level effects: rendering multiple draw commands to an isolated temporary surface, then compositing the result back with opacity, blend mode, or restricted bounds. This is the mechanism behind SVG `` and group blend modes. + +### Push Phase + +```text +SaveLayer(layerOptions, bounds) + 1. Flush() pending commands to current target + 2. Clamp bounds to canvas (min 1x1) + 3. Allocate transparent Image(width, height) + 4. Wrap in MemoryCanvasFrame + 5. Save current batcher in LayerData: + LayerData(parentBatcher, layerImage, layerFrame, layerBounds) + 6. Push LayerData onto layerDataStack + 7. Create new batcher targeting layer frame + 8. Push layer state onto savedStates: + DrawingCanvasState { IsLayer=true, LayerOptions, LayerBounds } + 9. Return SaveCount +``` + +### Draw Phase + +All commands between SaveLayer() and Restore() target the layer batcher, which writes to the temporary layer image. Clip paths, transforms, and drawing options apply normally within the layer's local coordinate space. + +### Pop Phase (Restore) + +```text +CompositeAndPopLayer(layerState) + 1. Flush() pending commands to layer surface + 2. Pop LayerData from layerDataStack + 3. Restore parent batcher: this.batcher = layerData.ParentBatcher + 4. Get destination from parent batcher's target frame + 5. backend.ComposeLayer(source=layerFrame, destination, bounds.Location, layerOptions) + 6. layerData.Dispose() (releases temporary image) +``` + +### Nesting + +Layers nest naturally via the `layerDataStack`. When compositing a nested layer, the parent batcher still targets the intermediate layer (not root). Each Restore pops one layer and composites it to its immediate parent. + +### Object Lifecycle Diagram + +```text +canvas.SaveLayer(options, bounds) + ├─ Image layerImage ──────────────┐ (allocated) + ├─ MemoryCanvasFrame layerFrame ──────────┤ + ├─ LayerData ────────────────────┤ + │ ├─ .ParentBatcher = old batcher │ + │ ├─ .LayerImage = layerImage │ + │ ├─ .LayerFrame = layerFrame │ + │ └─ .LayerBounds = clampedBounds │ + └─ new batcher → targets layerFrame │ + │ + ... draw commands → layer batcher ... │ + │ +canvas.Restore() │ + ├─ flush layer batcher │ + ├─ pop LayerData │ + ├─ restore parent batcher │ + ├─ ComposeLayer(layerFrame → parent) │ + └─ layerData.Dispose() ─────────────────┘ (freed) +``` + +## The Batcher + +`DrawingCanvasBatcher` queues `CompositionCommand` objects and flushes them to the backend. + +```text +DrawingCanvasBatcher + TargetFrame : ICanvasFrame (destination for this batcher) + AddComposition(command) (append to command list) + FlushCompositions() (create scene, call backend, clear list) +``` + +**Flush behavior:** +1. If no commands, return (no-op). +2. Create `CompositionScene` wrapping the command list. +3. Call `backend.FlushCompositions(configuration, targetFrame, scene)`. +4. Clear commands in `finally` block (always, even on failure). + +The canvas holds exactly one batcher at a time. SaveLayer swaps it for a new one targeting the layer frame; Restore swaps it back to the parent. + +## Drawing Operations + +### Fill + +```text +Fill(brush, path) + 1. ResolveState() → options, clipPaths + 2. path.AsClosedPath() + 3. Apply transform: path.Transform(options.Transform), brush.Transform(options.Transform) + 4. Apply clips: path.Clip(shapeOptions, clipPaths) + 5. PrepareCompositionCore(path, brush, options, PixelBoundary) + → batcher.AddComposition(CompositionCommand.Create(...)) +``` + +### Stroke (Draw) + +```text +Draw(pen, path) + 1. ResolveState() → options, clipPaths + 2. Apply transform to path + 3. Force NonZero winding rule (strokes self-overlap) + 4. If clip paths present: + → expand stroke to outline on CPU, then clip, then fill + 5. If no clip paths: + → PrepareStrokeCompositionCore(path, brush, strokeWidth, strokeOptions, ...) + → defers stroke expansion to backend + Uses PixelCenter sampling origin +``` + +### Clear + +Executes a fill with modified `GraphicsOptions` (Src composition, no blending) inside a temporary state scope via `ExecuteWithTemporaryState()`. + +### DrawImage + +Three-phase pipeline: +1. **Source preparation:** Crop/scale source image to destination dimensions. +2. **Transform application:** Apply canvas transform via `image.Transform()` with composed matrix. +3. **Deferred execution:** Transfer temp image to `pendingImageResources`, create `ImageBrush`, fill destination path. + +### DrawText + +Renders text to glyph operations via `RichTextGlyphRenderer`, sorts by render pass (for color font layers), submits commands in sorted order. + +### Process + +Flushes current commands, reads back pixels via `backend.TryReadRegion()`, runs an `Action` on the readback, then fills the result back to the canvas via `ImageBrush`. + +## Frame Abstraction + +```text +ICanvasFrame + Bounds : Rectangle + TryGetCpuRegion(out region) : bool + TryGetNativeSurface(out surface) : bool +``` + +| Implementation | CPU Region | Native Surface | Usage | +|---|---|---|---| +| `MemoryCanvasFrame` | Yes | No | CPU image buffers, layers | +| `NativeCanvasFrame` | No | Yes | GPU-backed surfaces | +| `CanvasRegionFrame` | Delegates | Delegates | Sub-region of parent frame | + +`CanvasRegionFrame` wraps a parent frame with a clipped rectangle, used by `CreateRegion()`. + +## Transform Handling + +Transforms are stored in `DrawingOptions.Transform` as `Matrix4x4` (default: Identity). Applied per-operation, not cumulatively. + +| Target | Method | +|---|---| +| Paths | `path.Transform(matrix)` | +| Brushes | `brush.Transform(matrix)` | +| Images | `image.Transform()` with composed source→destination→canvas matrix | + +## Clipping + +Clip paths are stored in `DrawingCanvasState.ClipPaths`. Applied during command preparation: + +```csharp +effectivePath = subjectPath.Clip(shapeOptions, clipPaths); +``` + +For strokes with clip paths, the stroke is expanded to an outline first, then clipped, then filled—this prevents clip artifacts at stroke edges. + +## CreateRegion + +```text +CreateRegion(region) + -> clamp to canvas bounds + -> wrap frame in CanvasRegionFrame + -> create child canvas: + - shares backend and batcher with parent + - snapshots current state + - isRoot = false (no resource release on dispose) + - local origin is (0,0) within clipped region +``` + +## Disposal + +```text +Dispose() + Phase 1: Pop all active layers + -> for each layer in layerDataStack (LIFO): + -> Flush() to layer surface + -> restore parent batcher + -> ComposeLayer(layer → parent) with default GraphicsOptions + -> layerData.Dispose() + + Phase 2: Final flush + -> batcher.FlushCompositions() + + Phase 3: Cleanup (in finally) + -> DisposePendingImageResources() + -> if isRoot: backend.ReleaseFrameResources(target) + -> isDisposed = true +``` + +Active layers that were never explicitly Restored are composited with default `GraphicsOptions` during disposal. This ensures no resource leaks, though the compositing result may differ from explicit Restore with custom options. + +## IDrawingBackend Interface + +```text +IDrawingBackend + IsSupported → bool (default true) + FlushCompositions (configuration, target, scene) + ComposeLayer (configuration, source, destination, offset, options) + TryReadRegion (configuration, target, rect, out image) → bool + ReleaseFrameResources (configuration, target) +``` + +`DefaultDrawingBackend` (singleton) handles all operations on CPU. `WebGPUDrawingBackend` accelerates FlushCompositions and ComposeLayer on GPU with CPU fallback. + +## Command Flow Summary + +```text +canvas.Fill(brush, path) + → transform path + brush + → clip path + → CompositionCommand.Create(path, brush, options) + → batcher.AddComposition(command) + → ... more draw calls ... + +canvas.Flush() or canvas.Dispose() + → batcher.FlushCompositions() + → CompositionScene(commands) + → backend.FlushCompositions(config, frame, scene) + → CompositionScenePlanner.CreatePreparedBatches() + → for each batch: rasterize + apply brushes + composite + → commands.Clear() + → DisposePendingImageResources() +``` diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 640c39c5e..069979c47 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -32,6 +32,11 @@ internal DrawingCanvasBatcher( this.targetFrame = targetFrame; } + /// + /// Gets the target frame that this batcher flushes to. + /// + public ICanvasFrame TargetFrame => this.targetFrame; + /// /// Appends one normalized composition command to the pending queue. /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs index 24a3821b1..374a2958b 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Processing.Backends; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -32,4 +34,60 @@ public DrawingCanvasState(DrawingOptions options, IReadOnlyList clipPaths /// Gets clip paths associated with this state. /// public IReadOnlyList ClipPaths { get; } + + /// + /// Gets a value indicating whether this state represents a compositing layer. + /// + public bool IsLayer { get; init; } + + /// + /// Gets the graphics options used to composite this layer on restore. + /// Only set when is . + /// + public GraphicsOptions? LayerOptions { get; init; } + + /// + /// Gets the local bounds of this layer relative to the parent canvas. + /// Only set when is . + /// + public Rectangle? LayerBounds { get; init; } +} + +/// +/// Typed layer data that holds the parent batcher and temporary layer buffer. +/// +/// The pixel format. +internal sealed class LayerData + where TPixel : unmanaged, IPixel +{ + /// + /// Initializes a new instance of the class. + /// + /// The parent batcher to restore on layer pop. + /// The canvas frame wrapping the layer buffer. + /// The local bounds of this layer relative to the parent. + public LayerData( + DrawingCanvasBatcher parentBatcher, + ICanvasFrame layerFrame, + Rectangle layerBounds) + { + this.ParentBatcher = parentBatcher; + this.LayerFrame = layerFrame; + this.LayerBounds = layerBounds; + } + + /// + /// Gets the batcher that was active before this layer was pushed. + /// + public DrawingCanvasBatcher ParentBatcher { get; } + + /// + /// Gets the canvas frame wrapping the layer buffer. + /// + public ICanvasFrame LayerFrame { get; } + + /// + /// Gets the local bounds of this layer relative to the parent canvas. + /// + public Rectangle LayerBounds { get; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 0f7d22689..471842bc6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -39,8 +39,15 @@ public sealed class DrawingCanvas : IDrawingCanvas /// /// Command batcher used to defer and submit composition commands. + /// Reassigned when a layer is pushed or popped via . /// - private readonly DrawingCanvasBatcher batcher; + private DrawingCanvasBatcher batcher; + + /// + /// Active layer data stack. Each entry corresponds to a + /// call on the saved-states stack and holds the parent batcher and temporary layer buffer. + /// + private readonly Stack> layerDataStack = new(); /// /// Temporary image resources that must stay alive until queued commands are flushed. @@ -233,7 +240,11 @@ public static DrawingCanvas FromRootFrame( public int Save() { this.EnsureNotDisposed(); - this.savedStates.Push(this.ResolveState()); + DrawingCanvasState current = this.ResolveState(); + + // Push a non-layer copy of the current state. + // Only states pushed by SaveLayer() should trigger layer compositing on restore. + this.savedStates.Push(new DrawingCanvasState(current.Options, current.ClipPaths)); return this.savedStates.Count; } @@ -251,6 +262,59 @@ public int Save(DrawingOptions options, params IPath[] clipPaths) return this.savedStates.Count; } + /// + public int SaveLayer() + => this.SaveLayer(new GraphicsOptions()); + + /// + public int SaveLayer(GraphicsOptions layerOptions) + => this.SaveLayer(layerOptions, this.Bounds); + + /// + public int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(layerOptions, nameof(layerOptions)); + + // Flush any pending commands to the current target before switching. + this.Flush(); + + // Clamp bounds to the canvas. + Rectangle layerBounds = Rectangle.Intersect(this.Bounds, bounds); + if (layerBounds.Width <= 0 || layerBounds.Height <= 0) + { + layerBounds = new Rectangle(0, 0, 1, 1); + } + + // Allocate a layer frame via the backend (CPU image or GPU texture). + ICanvasFrame currentTarget = this.batcher.TargetFrame; + ICanvasFrame layerFrame = this.backend.CreateLayerFrame( + this.configuration, + currentTarget, + layerBounds.Width, + layerBounds.Height); + + // Save the current batcher so we can restore it later. + DrawingCanvasBatcher parentBatcher = this.batcher; + LayerData layerData = new(parentBatcher, layerFrame, layerBounds); + this.layerDataStack.Push(layerData); + + // Redirect commands to the layer target. + this.batcher = new DrawingCanvasBatcher(this.configuration, this.backend, layerFrame); + + // Push a layer state onto the saved states stack. + DrawingCanvasState currentState = this.ResolveState(); + DrawingCanvasState layerState = new(currentState.Options, currentState.ClipPaths) + { + IsLayer = true, + LayerOptions = layerOptions, + LayerBounds = layerBounds, + }; + + this.savedStates.Push(layerState); + return this.savedStates.Count; + } + /// public void Restore() { @@ -260,7 +324,11 @@ public void Restore() return; } - _ = this.savedStates.Pop(); + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + this.CompositeAndPopLayer(popped); + } } /// @@ -271,7 +339,11 @@ public void RestoreTo(int saveCount) while (this.savedStates.Count > saveCount) { - _ = this.savedStates.Pop(); + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + this.CompositeAndPopLayer(popped); + } } } @@ -1125,17 +1197,28 @@ public void Dispose() try { + // Composite any active layers back before final flush. + while (this.layerDataStack.Count > 0) + { + this.Flush(); + LayerData layerData = this.layerDataStack.Pop(); + this.batcher = layerData.ParentBatcher; + ICanvasFrame destination = this.batcher.TargetFrame; + this.backend.ComposeLayer( + this.configuration, + layerData.LayerFrame, + destination, + layerData.LayerBounds.Location, + new GraphicsOptions()); + this.backend.ReleaseFrameResources(this.configuration, layerData.LayerFrame); + } + this.batcher.FlushCompositions(); } finally { this.DisposePendingImageResources(); - if (this.isRoot) - { - this.backend.ReleaseFrameResources(this.configuration, this.targetFrame); - } - this.isDisposed = true; } } @@ -1146,6 +1229,36 @@ public void Dispose() private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + /// + /// Flushes the current layer batcher, composites the layer onto its parent target, + /// restores the parent batcher, and disposes the layer resources. + /// + /// The layer state that was just popped. + private void CompositeAndPopLayer(DrawingCanvasState layerState) + { + // Flush pending commands to the layer surface. + this.Flush(); + + LayerData layerData = this.layerDataStack.Pop(); + + // Restore the parent batcher. + this.batcher = layerData.ParentBatcher; + + // Composite the layer onto the parent batcher's target (which may be another layer + // in the case of nested SaveLayer calls, or the root target frame). + ICanvasFrame destination = this.batcher.TargetFrame; + GraphicsOptions options = layerState.LayerOptions ?? new GraphicsOptions(); + Rectangle bounds = layerState.LayerBounds ?? this.Bounds; + this.backend.ComposeLayer( + this.configuration, + layerData.LayerFrame, + destination, + bounds.Location, + options); + + this.backend.ReleaseFrameResources(this.configuration, layerData.LayerFrame); + } + /// /// Normalizes text options to avoid applying origin translation twice when path-based text is used. /// diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs index 31157cd20..bd9fb9f43 100644 --- a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -45,9 +45,51 @@ public interface IDrawingCanvas : IDisposable /// The save count after the previous state has been pushed. public int Save(DrawingOptions options, params IPath[] clipPaths); + /// + /// Saves the current drawing state and begins an isolated compositing layer. + /// Subsequent draw commands render to a temporary surface. When + /// is called, the layer is composited back onto the parent using the default + /// . + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(); + + /// + /// Saves the current drawing state and begins an isolated compositing layer. + /// Subsequent draw commands render to a temporary surface. When + /// is called, the layer is composited back onto the parent using the specified + /// (blend mode, alpha composition, opacity). + /// + /// + /// Graphics options controlling how the layer is composited on restore. + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(GraphicsOptions layerOptions); + + /// + /// Saves the current drawing state and begins an isolated compositing layer + /// bounded to a subregion. Subsequent draw commands render to a temporary surface + /// sized to . When is called, the + /// layer is composited back onto the parent using the specified + /// . + /// + /// + /// Graphics options controlling how the layer is composited on restore. + /// + /// + /// The local bounds of the layer. Only this region is allocated and composited. + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds); + /// /// Restores the most recently saved state. /// + /// + /// If the most recently saved state was created by a SaveLayer overload, + /// pending commands are flushed to the layer surface, the layer is composited back onto + /// the parent target, and the temporary layer buffer is disposed. + /// public void Restore(); /// @@ -56,6 +98,8 @@ public interface IDrawingCanvas : IDisposable /// /// State frames above are discarded, /// and the last discarded frame becomes the current state. + /// If any discarded state was created by a SaveLayer overload, + /// the layer is composited and its resources disposed. /// /// The save count to restore to. public void RestoreTo(int saveCount); diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index f205e6548..02f65b8bd 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -23,7 +23,6 @@ public abstract class DrawPolygon private PointF[][] points; private Image image; - private Image webGpuImage; private Bitmap sdBitmap; private Graphics sdGraphics; @@ -116,7 +115,6 @@ public void Setup() this.webGpuBackend = new WebGPUDrawingBackend(); this.webGpuConfiguration = Configuration.Default.Clone(); this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); - this.webGpuImage = new Image(this.webGpuConfiguration, this.Width, this.Height); if (!WebGPUTestNativeSurfaceAllocator.TryCreate( this.Width, @@ -174,7 +172,6 @@ public void Cleanup() this.skPath.Dispose(); this.image.Dispose(); - this.webGpuImage.Dispose(); WebGPUTestNativeSurfaceAllocator.Release( this.webGpuNativeTextureHandle, this.webGpuNativeTextureViewHandle); @@ -195,10 +192,6 @@ public void SystemDrawing() public void ImageSharp() => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); - [Benchmark] - public void ImageSharpCWebGPUMemoryBuffer() - => this.webGpuImage.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); - [Benchmark] public void ImageSharpWebGPUNativeSurface() { diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs index 68961bf21..4f9403a6d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -30,7 +30,6 @@ public class DrawTextRepeatedGlyphs private Configuration defaultConfiguration; private Image defaultImage; - private Image webGpuCpuImage; private WebGPUDrawingBackend webGpuBackend; private Configuration webGpuConfiguration; private NativeCanvasFrame webGpuNativeFrame; @@ -64,7 +63,6 @@ public void Setup() this.webGpuBackend = new WebGPUDrawingBackend(); this.webGpuConfiguration = Configuration.Default.Clone(); this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); - this.webGpuCpuImage = new Image(this.webGpuConfiguration, Width, Height); if (!WebGPUTestNativeSurfaceAllocator.TryCreate( Width, @@ -89,7 +87,6 @@ public void Setup() public void Cleanup() { this.defaultImage.Dispose(); - this.webGpuCpuImage.Dispose(); WebGPUTestNativeSurfaceAllocator.Release( this.webGpuNativeTextureHandle, this.webGpuNativeTextureViewHandle); @@ -108,16 +105,6 @@ public void DrawingCanvasDefaultBackend() canvas.Flush(); } - [Benchmark(Description = "DrawingCanvas WebGPU Backend (CPURegion)")] - public void DrawingCanvasWebGPUBackendCpuRegion() - { - MemoryCanvasFrame frame = new(GetFrameRegion(this.webGpuCpuImage)); - - using DrawingCanvas canvas = new(this.webGpuConfiguration, frame, this.drawingOptions); - canvas.DrawText(this.textOptions, this.text, this.brush, null); - canvas.Flush(); - } - [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] public void DrawingCanvasWebGPUBackendNativeSurface() { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index d8c4ee9d3..c20feca84 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; [GroupOutput("Drawing")] -public class WebGPUDrawingBackendTests +public partial class WebGPUDrawingBackendTests { public static TheoryData GraphicsOptionsModePairs { get; } = new() @@ -49,10 +49,6 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -63,26 +59,15 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath", defaultImage, cpuRegionImage, nativeSurfaceImage); - - Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); - Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(cpuRegionBackend); - if (cpuRegionBackend.TestingIsGPUReady) - { - Assert.True(cpuRegionBackend.TestingGPUPrepareCoverageCallCount > 0); - Assert.True(cpuRegionBackend.TestingGPUCompositeCoverageCallCount + cpuRegionBackend.TestingFallbackCompositeCoverageCallCount > 0); - } + DebugSaveBackendPair(provider, "FillPath", defaultImage, nativeSurfaceImage); Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } [WebGPUTheory] @@ -103,10 +88,6 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -117,12 +98,10 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_AliasedThreshold", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendPair(provider, "FillPath_AliasedThreshold", defaultImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } [WebGPUTheory] @@ -149,10 +128,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = new(384, 256); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = new(384, 256); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, @@ -161,16 +136,7 @@ void DrawAction(DrawingCanvas canvas) drawingOptions, (Action>)DrawAction); - DebugSaveBackendTriplet(provider, "FillPath_ImageBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); - - Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); - Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(cpuRegionBackend); - if (cpuRegionBackend.TestingIsGPUReady) - { - Assert.True(cpuRegionBackend.TestingGPUCompositeCoverageCallCount > 0); - } + DebugSaveBackendPair(provider, "FillPath_ImageBrush", defaultImage, nativeSurfaceImage); Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); @@ -181,9 +147,8 @@ void DrawAction(DrawingCanvas canvas) Assert.True(nativeSurfaceBackend.TestingGPUCompositeCoverageCallCount > 0); } - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } [WebGPUTheory] @@ -230,10 +195,6 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -244,26 +205,19 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_NonZeroNestedContours", defaultImage, cpuRegionImage, nativeSurfaceImage); - - Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); - Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); - Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_NonZeroNestedContours", defaultImage, nativeSurfaceImage); Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); Assert.Equal(nativeSurfaceBackend.TestingPrepareCoverageCallCount, nativeSurfaceBackend.TestingReleaseCoverageCallCount); Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); // Non-zero winding semantics must still match on an interior point. - Assert.Equal(defaultImage[128, 128], cpuRegionImage[128, 128]); Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); } [WebGPUTheory] @@ -294,10 +248,6 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput defaultImage = baseImage.Clone(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = baseImage.Clone(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, @@ -307,18 +257,15 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput defaultImage = baseImage.Clone(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = baseImage.Clone(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, @@ -363,18 +306,15 @@ public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -415,13 +351,7 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "DrawText", defaultImage, cpuRegionImage, nativeSurfaceImage); - - Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount > 0); - Assert.True(cpuRegionBackend.TestingCompositeCoverageCallCount >= cpuRegionBackend.TestingPrepareCoverageCallCount); - Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); - Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "DrawText", defaultImage, nativeSurfaceImage); Assert.True(nativeSurfaceBackend.TestingPrepareCoverageCallCount > 0); Assert.True(nativeSurfaceBackend.TestingCompositeCoverageCallCount >= nativeSurfaceBackend.TestingPrepareCoverageCallCount); @@ -429,14 +359,13 @@ public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverag Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.007F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); Rectangle textRegion = Rectangle.Intersect( new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); - AssertBackendTripletSimilarityInRegion(defaultImage, cpuRegionImage, nativeSurfaceImage, textRegion, 0.009F); + AssertBackendPairSimilarityInRegion(defaultImage, nativeSurfaceImage, textRegion, 0.009F); } [WebGPUTheory] @@ -461,10 +390,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -475,12 +400,10 @@ void DrawAction(DrawingCanvas canvas) DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceParity", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_NativeSurfaceParity", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); } [WebGPUTheory] @@ -507,10 +430,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -521,12 +440,10 @@ void DrawAction(DrawingCanvas canvas) DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); } [WebGPUTheory] @@ -547,10 +464,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( defaultImage.Width, @@ -559,14 +472,12 @@ void DrawAction(DrawingCanvas canvas) drawingOptions, (Action>)DrawAction); - DebugSaveBackendTriplet(provider, "Process", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "Process", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); // Differences are visually allowable so use a higher threshold here. - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.0516F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0516F); } [WebGPUTheory] @@ -593,10 +504,6 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -607,14 +514,8 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "RepeatedGlyphs", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 2F); - - Assert.InRange(cpuRegionBackend.TestingPrepareCoverageCallCount, 1, 20); - Assert.True(cpuRegionBackend.TestingCompositeCoverageCallCount >= cpuRegionBackend.TestingPrepareCoverageCallCount); - Assert.Equal(cpuRegionBackend.TestingPrepareCoverageCallCount, cpuRegionBackend.TestingReleaseCoverageCallCount); - Assert.Equal(0, cpuRegionBackend.TestingLiveCoverageCount); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "RepeatedGlyphs", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); Assert.InRange(nativeSurfaceBackend.TestingPrepareCoverageCallCount, 1, 20); Assert.True(nativeSurfaceBackend.TestingCompositeCoverageCallCount >= nativeSurfaceBackend.TestingPrepareCoverageCallCount); @@ -622,7 +523,6 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi Assert.Equal(0, nativeSurfaceBackend.TestingLiveCoverageCount); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); } @@ -671,26 +571,6 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes defaultDrawCanvas.Flush(); } - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - Configuration cpuRegionConfiguration = Configuration.Default.Clone(); - cpuRegionConfiguration.SetDrawingBackend(cpuRegionBackend); - - using (DrawingCanvas cpuRegionClearCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage), clearOptions)) - { - cpuRegionClearCanvas.Fill(clearBrush); - cpuRegionClearCanvas.Flush(); - } - - int cpuRegionComputeBatchesBeforeDraw = cpuRegionBackend.TestingComputePathBatchCount; - using (DrawingCanvas cpuRegionDrawCanvas = new(cpuRegionConfiguration, GetFrameRegion(cpuRegionImage), drawingOptions)) - { - cpuRegionDrawCanvas.DrawText(textOptions, text, drawBrush, null); - cpuRegionDrawCanvas.Flush(); - } - - int cpuRegionComputeBatchesFromDraw = cpuRegionBackend.TestingComputePathBatchCount - cpuRegionComputeBatchesBeforeDraw; - using WebGPUDrawingBackend nativeSurfaceBackend = new(); Assert.True( WebGPUTestNativeSurfaceAllocator.TryCreate( @@ -737,20 +617,12 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(Tes using (nativeSurfaceImage) { - DebugSaveBackendTriplet(provider, "RepeatedGlyphs_AfterClear", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 2F); + DebugSaveBackendPair(provider, "RepeatedGlyphs_AfterClear", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); } - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - if (cpuRegionBackend.TestingIsGPUReady) - { - Assert.True( - cpuRegionComputeBatchesFromDraw > 0, - "Expected repeated-glyph draw batch to execute via tiled compute composition on the CPURegion pipeline."); - } - if (nativeSurfaceBackend.TestingIsGPUReady) { Assert.True( @@ -813,20 +685,6 @@ private static IPath CreatePixelateTrianglePath() return pathBuilder.Build(); } - private static void RenderWithCpuRegionWebGpuBackend( - Image image, - WebGPUDrawingBackend backend, - DrawingOptions options, - Action> drawAction) - where TPixel : unmanaged, IPixel - { - Configuration configuration = Configuration.Default.Clone(); - configuration.SetDrawingBackend(backend); - using DrawingCanvas canvas = new(configuration, GetFrameRegion(image), options); - drawAction(canvas); - canvas.Flush(); - } - private static Image RenderWithNativeSurfaceWebGpuBackend( int width, int height, @@ -886,11 +744,10 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( } } - private static void DebugSaveBackendTriplet( + private static void DebugSaveBackendPair( TestImageProvider provider, string testName, Image defaultImage, - Image cpuRegionImage, Image nativeSurfaceImage, float tolerantPercentage = 0.0003F) where TPixel : unmanaged, IPixel @@ -901,12 +758,6 @@ private static void DebugSaveBackendTriplet( appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - cpuRegionImage.DebugSave( - provider, - $"{testName}_WebGPU_CPURegion", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - nativeSurfaceImage.DebugSave( provider, $"{testName}_WebGPU_NativeSurface", @@ -921,13 +772,6 @@ private static void DebugSaveBackendTriplet( appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - cpuRegionImage.CompareToReferenceOutput( - tolerantComparer, - provider, - $"{testName}_WebGPU_CPURegion", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - nativeSurfaceImage.CompareToReferenceOutput( tolerantComparer, provider, @@ -936,11 +780,10 @@ private static void DebugSaveBackendTriplet( appendSourceFileOrDescription: false); } - private static void DebugSaveBackendTripletNoRef( + private static void DebugSaveBackendPairNoRef( TestImageProvider provider, string testName, Image defaultImage, - Image cpuRegionImage, Image nativeSurfaceImage) where TPixel : unmanaged, IPixel { @@ -950,12 +793,6 @@ private static void DebugSaveBackendTripletNoRef( appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - cpuRegionImage.DebugSave( - provider, - $"{testName}_WebGPU_CPURegion", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - nativeSurfaceImage.DebugSave( provider, $"{testName}_WebGPU_NativeSurface", @@ -963,31 +800,26 @@ private static void DebugSaveBackendTripletNoRef( appendSourceFileOrDescription: false); } - private static void AssertBackendTripletSimilarity( + private static void AssertBackendPairSimilarity( Image defaultImage, - Image cpuRegionImage, Image nativeSurfaceImage, float defaultTolerancePercent) where TPixel : unmanaged, IPixel { - ImageComparer.TolerantPercentage(0.01F).VerifySimilarity(cpuRegionImage, nativeSurfaceImage); ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(defaultTolerancePercent); - tolerantComparer.VerifySimilarity(defaultImage, cpuRegionImage); tolerantComparer.VerifySimilarity(defaultImage, nativeSurfaceImage); } - private static void AssertBackendTripletSimilarityInRegion( + private static void AssertBackendPairSimilarityInRegion( Image defaultImage, - Image cpuRegionImage, Image nativeSurfaceImage, Rectangle region, float defaultTolerancePercent) where TPixel : unmanaged, IPixel { using Image defaultRegion = defaultImage.Clone(ctx => ctx.Crop(region)); - using Image cpuRegion = cpuRegionImage.Clone(ctx => ctx.Crop(region)); using Image nativeRegion = nativeSurfaceImage.Clone(ctx => ctx.Crop(region)); - AssertBackendTripletSimilarity(defaultRegion, cpuRegion, nativeRegion, defaultTolerancePercent); + AssertBackendPairSimilarity(defaultRegion, nativeRegion, defaultTolerancePercent); } private static void AssertCoverageExecutionAccounting(WebGPUDrawingBackend backend) @@ -1050,9 +882,6 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1064,12 +893,10 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider LineJoinValues { get; } = new() @@ -1113,9 +940,6 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvi using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1127,12 +951,10 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvi DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); } [WebGPUTheory] @@ -1163,9 +985,6 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1177,12 +996,10 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.01F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); } [WebGPUTheory] @@ -1209,9 +1026,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1223,14 +1037,12 @@ void DrawAction(DrawingCanvas canvas) DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_MultipleSeparate", defaultImage, cpuRegionImage, nativeSurfaceImage); + DebugSaveBackendPair(provider, "FillPath_MultipleSeparate", defaultImage, nativeSurfaceImage); + - Assert.True(cpuRegionBackend.TestingPrepareCoverageCallCount >= 20); - AssertCoverageExecutionAccounting(cpuRegionBackend); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } [WebGPUTheory] @@ -1276,9 +1088,6 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1290,16 +1099,13 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_EvenOdd", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_EvenOdd", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); // EvenOdd with same winding inner contour should create a hole at center. - Assert.Equal(defaultImage[128, 128], cpuRegionImage[128, 128]); Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); } [WebGPUTheory] @@ -1320,9 +1126,6 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1334,12 +1137,10 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_LargeTileCount", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_LargeTileCount", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } [WebGPUTheory] @@ -1371,24 +1172,6 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag canvas2.Flush(); } - // WebGPU backend: two separate flushes reusing the same backend - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - Configuration cpuConfig = Configuration.Default.Clone(); - cpuConfig.SetDrawingBackend(cpuRegionBackend); - - using (DrawingCanvas canvas1 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) - { - canvas1.Fill(redBrush, rect1); - canvas1.Flush(); - } - - using (DrawingCanvas canvas2 = new(cpuConfig, GetFrameRegion(cpuRegionImage), drawingOptions)) - { - canvas2.Fill(blueBrush, rect2); - canvas2.Flush(); - } - // Native surface: two separate flushes reusing same backend using WebGPUDrawingBackend nativeSurfaceBackend = new(); Assert.True( @@ -1443,12 +1226,10 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImag using (nativeSurfaceImage) { - DebugSaveBackendTriplet(provider, "MultipleFlushes", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "MultipleFlushes", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } } finally @@ -1481,9 +1262,6 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1496,12 +1274,10 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm nativeSurfaceInitialImage); // MacOS on CI has some outliers with this test, so using a slightly higher tolerance here to avoid noise. - DebugSaveBackendTriplet(provider, "FillPath_LinearGradient", defaultImage, cpuRegionImage, nativeSurfaceImage, tolerantPercentage: 0.0007F); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_LinearGradient", defaultImage, nativeSurfaceImage, tolerantPercentage: 0.0007F); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1527,9 +1303,6 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1541,12 +1314,10 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_LinearGradient_Repeat", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_LinearGradient_Repeat", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1572,9 +1343,6 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1586,12 +1354,10 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1633,12 +1396,10 @@ public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1679,12 +1437,10 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(Test DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_EllipticGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_EllipticGradient", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1713,9 +1469,6 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1727,12 +1480,10 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_SweepGradient", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_SweepGradient", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1759,9 +1510,6 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1774,17 +1522,14 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput(TestImageProv using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1820,12 +1562,10 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProv DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_PatternBrush_Horizontal", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_PatternBrush_Horizontal", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1846,9 +1586,6 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1860,12 +1597,10 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(Test DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_PatternBrush_Diagonal", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_PatternBrush_Diagonal", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1886,9 +1621,6 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1900,12 +1632,10 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "FillPath_RecolorBrush", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "FillPath_RecolorBrush", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); } [WebGPUTheory] @@ -1932,9 +1662,6 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1946,12 +1673,10 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -1993,12 +1715,10 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using Image cpuRegionImage = provider.GetImage(); - using WebGPUDrawingBackend cpuRegionBackend = new(); - RenderWithCpuRegionWebGpuBackend(cpuRegionImage, cpuRegionBackend, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); @@ -2227,12 +1944,236 @@ void DrawAction(DrawingCanvas canvas) DrawAction, nativeSurfaceInitialImage); - DebugSaveBackendTriplet(provider, "StarWarsCrawl", defaultImage, cpuRegionImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(cpuRegionBackend); + DebugSaveBackendPair(provider, "StarWarsCrawl", defaultImage, nativeSurfaceImage); AssertCoverageExecutionAccounting(nativeSurfaceBackend); - AssertGpuPathWhenRequired(cpuRegionBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); - AssertBackendTripletSimilarity(defaultImage, cpuRegionImage, nativeSurfaceImage, 0.005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_FullOpacity_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Red); + RectangularPolygon polygon = new(10, 10, 80, 80); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.SaveLayer(); + canvas.Fill(brush, polygon); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_FullOpacity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_HalfOpacity_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Red); + RectangularPolygon polygon = new(10, 10, 80, 80); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(brush, polygon); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_HalfOpacity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_NestedLayers_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Outer layer: red fill. + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(0, 0, 128, 128)); + + // Inner layer: blue fill over center. + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Blue), new RectangularPolygon(32, 32, 64, 64)); + canvas.Restore(); // Composites blue onto red. + + canvas.Restore(); // Composites red+blue onto white. + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_NestedLayers", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_WithBlendMode_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(20, 20, 88, 88)); + + canvas.SaveLayer(new GraphicsOptions + { + ColorBlendingMode = PixelColorBlendingMode.Multiply, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver, + BlendPercentage = 1f + }); + + canvas.Fill(Brushes.Solid(Color.Blue), new RectangularPolygon(40, 40, 88, 88)); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_WithBlendMode", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_WithBounds_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Layer restricted to a sub-region; draw within the layer's local bounds. + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(16, 16, 96, 96)); + canvas.Fill(Brushes.Solid(Color.Green), new RectangularPolygon(0, 0, 96, 96)); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_WithBounds", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + int before = canvas.SaveCount; + canvas.Save(); // plain save + canvas.SaveLayer(); // layer + canvas.Save(); // plain save + + canvas.Fill(Brushes.Solid(Color.Green), new RectangularPolygon(0, 0, 128, 128)); + + canvas.RestoreTo(before); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_MixedSaveAndSaveLayer", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); } private static Buffer2DRegion GetFrameRegion(Image image) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index b18e869e3..cde8f18a5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -174,6 +174,24 @@ public bool TryReadRegion( return false; } + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + } + + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.CreateLayerFrame(configuration, parentTarget, width, height); + public void ReleaseFrameResources( Configuration configuration, ICanvasFrame target) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index cc2986a76..1994f46b7 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -223,6 +223,23 @@ public bool TryReadRegion( return true; } + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TTargetPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.ComposeLayer(configuration, source, destination, destinationOffset, options); + + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TTargetPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.CreateLayerFrame(configuration, parentTarget, width, height); + public void ReleaseFrameResources( Configuration configuration, ICanvasFrame target) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs new file mode 100644 index 000000000..c4d0176ad --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs @@ -0,0 +1,198 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void SaveLayer_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + int count = canvas.SaveLayer(); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_WithOptions_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_WithBounds_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.SaveLayer(new GraphicsOptions(), new Rectangle(10, 10, 32, 32)); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_Restore_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.SaveLayer(); + Assert.Equal(2, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_RestoreTo_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int before = canvas.SaveCount; + canvas.SaveLayer(); + canvas.Save(); + Assert.Equal(3, canvas.SaveCount); + + canvas.RestoreTo(before); + Assert.Equal(before, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_DrawAndRestore_CompositesLayerOntoTarget() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + // Fill background white. + canvas.Fill(new SolidBrush(Color.White)); + + // SaveLayer with full opacity, draw red rectangle, then restore. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(10, 10, 20, 20)); + canvas.Restore(); + + // The red rectangle should be composited onto the white background. + Rgba32 center = target[20, 20]; + Assert.Equal(new Rgba32(255, 0, 0, 255), center); + + // Outside the filled region should remain white. + Rgba32 corner = target[0, 0]; + Assert.Equal(new Rgba32(255, 255, 255, 255), corner); + } + + [Fact] + public void SaveLayer_WithHalfOpacity_CompositesWithBlend() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + // Fill background white. + canvas.Fill(new SolidBrush(Color.White)); + + // SaveLayer with 50% opacity, draw red rectangle, then restore. + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(10, 10, 20, 20)); + canvas.Restore(); + + // The red should be blended at ~50% onto white, giving approximately (255, 128, 128). + Rgba32 center = target[20, 20]; + Assert.InRange(center.R, 120, 255); + Assert.InRange(center.G, 100, 140); + Assert.InRange(center.B, 100, 140); + } + + [Fact] + public void SaveLayer_Dispose_CompositesActiveLayer() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + + // Create canvas, push a layer, draw, and dispose without explicit Restore. + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(0, 0, 32, 32)); + + // Dispose should composite the layer. + } + + // After dispose, the blue fill should be visible. + Rgba32 pixel = target[16, 16]; + Assert.Equal(new Rgba32(0, 0, 255, 255), pixel); + } + + [Fact] + public void SaveLayer_NestedLayers_CompositeCorrectly() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Fill(new SolidBrush(Color.White)); + + // Outer layer. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(0, 0, 64, 64)); + + // Inner layer. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(16, 16, 32, 32)); + canvas.Restore(); // Composites blue onto red. + + canvas.Restore(); // Composites red+blue onto white. + + // Center should be blue (inner layer overwrites outer). + Rgba32 center = target[32, 32]; + Assert.Equal(new Rgba32(0, 0, 255, 255), center); + + // Corner should be red (outer layer only). + Rgba32 corner = target[5, 5]; + Assert.Equal(new Rgba32(255, 0, 0, 255), corner); + } + + [Fact] + public void SaveLayer_MixedSaveAndSaveLayer_WorksCorrectly() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Fill(new SolidBrush(Color.White)); + + canvas.Save(); // SaveCount = 2 (plain save) + canvas.SaveLayer(); // SaveCount = 3 (layer) + canvas.Save(); // SaveCount = 4 (plain save) + Assert.Equal(4, canvas.SaveCount); + + canvas.Fill(new SolidBrush(Color.Green), new RectangularPolygon(0, 0, 64, 64)); + + // RestoreTo(1) should pop all states including the layer. + canvas.RestoreTo(1); + Assert.Equal(1, canvas.SaveCount); + + Rgba32 pixel = target[32, 32]; + Assert.Equal(new Rgba32(0, 128, 0, 255), pixel); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 249babc7a..07117d628 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -66,6 +66,24 @@ public bool TryReadRegion( return false; } + public void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + } + + public ICanvasFrame CreateLayerFrame( + Configuration configuration, + ICanvasFrame parentTarget, + int width, + int height) + where TPixel : unmanaged, IPixel + => DefaultDrawingBackend.Instance.CreateLayerFrame(configuration, parentTarget, width, height); + public void ReleaseFrameResources( Configuration configuration, ICanvasFrame target) diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png index ed5c8f4a9..1406aa4a3 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:005950a4aff24fb25f8a7db370770277c4dbbfafb9e759d21f7a8545bac76941 -size 19785 +oid sha256:b8ca107a5968ff2c14ecc4c3e6264123cf25148074734360552bb6ca13d08a8b +size 12904 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png index 92636e503..7d7de4506 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d5c55275454be86d1a502d0f5d4c601f6833447a6585d27255a200fbeff503d -size 33142 +oid sha256:c6b5966124275a574cc12f09f333a60e3a9c72e68a4e4304908dc00bf8e7680f +size 27575 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png index 20845d0e6..c2d5e2a13 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d81fbec8890cdd62f4cf4ed5df3742441468d7df3ac3555435ef9f6df287000 -size 1134 +oid sha256:eb7e8e4ae30d344156c60959885911420001917fe8ab3c1ca8e5439a1b755b4e +size 1131 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png index 143ed026c..4f8cc9dbd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0028dcad66a54d6bcad2b7f84e388b8bc71c65faf3e78afa74a00b348716bbb0 -size 1972 +oid sha256:b676834eb7a429b08ac4a437f00de60f3511eef118887a3c0650bde750af1983 +size 1971 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png index a85447471..81b15d01e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e6e3250beaa281fbbf35f2226cb850df1572aec477cdf438467a43332858ed8 -size 246 +oid sha256:6edbc17395e460588a4fbb50b430e4b7979c5f25b2f521d8a6fbb972ca9107d5 +size 309 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png deleted file mode 100644 index 0ab0b108f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b71e2d978dd559a71c9559fbb77ac756faeb3e73c6e6beef8b35c29eaed13f23 -size 31691 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png deleted file mode 100644 index b1654d308..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92cb29d99646955e70bcd07b0061471c3168dd471f6d0280bebfbb48f1b5dedc -size 957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png deleted file mode 100644 index 09b1672a6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:420a03c8f1f59cba44111ece194b71958d2d519228e1c26427e02cca87173fd0 -size 1083 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png deleted file mode 100644 index c2056aacb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6320f82d43ac4c8474cba384cae43217c2abaec77762d754d4c0d13465b6743b -size 869 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png deleted file mode 100644 index a37448cd3..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a7aa551c29f2eef2434f878c1241e374b74cda4347b41c8899e597e12a4a78ec -size 3323 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png deleted file mode 100644 index a4b78daf4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e -size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png deleted file mode 100644 index a4b78daf4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e -size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png deleted file mode 100644 index a4b78daf4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79db4f813ef3bc5364cadc2953e13a2541c2e136945eae5d402611ac04700c2e -size 3499 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png deleted file mode 100644 index e656f164b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a28a8b235e21f19b9b2dd35f666fec1f21c31da9c5b84732ad1c9243fc78c5cc -size 3419 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png deleted file mode 100644 index f0500c2d7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac7a7ae06eb322c60aa1faad8c880df71e943ae37948ccf50dd2394a044012f3 -size 2264 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png deleted file mode 100644 index 0fe683c1d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e06b27fd2bb367ac3ed7c57777a5073ccacc99ba99bbbdfef238f905a7c58cee -size 4693 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png deleted file mode 100644 index 7024ebfef..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c387a6f663c4badd82784e90e020a9c5aa5cc8a1486cd7570c6a41dee0e88ab8 -size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png deleted file mode 100644 index 9200bf044..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2e29507a7e569a2f0a887f196719b5943495184d9b33c8319760bcc78a9a0a7 -size 36329 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png deleted file mode 100644 index 71fe4495e..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 -size 523 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png deleted file mode 100644 index 09adf1c65..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 -size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png deleted file mode 100644 index b37aefc21..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7921accb83e6cc2c3b537a0c354a403d15cfcad95aa1e352d5811a2082a69fc3 -size 6093 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png deleted file mode 100644 index 3b25b6ccb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d -size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png deleted file mode 100644 index 0312b3f84..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37df4c3c1e2174e4ce5ccc1cf56463e0232e8147106e996c432233eb56f31b48 -size 4349 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png deleted file mode 100644 index 8bf73279f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca53253afc2f40e32a424bf6deb0791f3d688baacaf8c86662cea97dd5fd0dea -size 38880 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png deleted file mode 100644 index 1a905ff83..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 -size 566 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png deleted file mode 100644 index 1b1ed3e3b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c -size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png deleted file mode 100644 index 0d51f838c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd -size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png deleted file mode 100644 index 00a793ec2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 -size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png deleted file mode 100644 index 443c5e78e..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de -size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png deleted file mode 100644 index d835b86af..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png deleted file mode 100644 index 0627f844b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f -size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png deleted file mode 100644 index 71d06c28f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a -size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png deleted file mode 100644 index d8b6ebb18..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 -size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png deleted file mode 100644 index 212ff2e1a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:448effa8e8cac551ffb3f849109f07291e575cbcd0c6e8bb4cc7b8c514772312 -size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png deleted file mode 100644 index bcc59e5ae..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 -size 472 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png deleted file mode 100644 index ff3590331..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png deleted file mode 100644 index c561128ef..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png deleted file mode 100644 index 43394d294..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png deleted file mode 100644 index 31b07cb56..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 -size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png deleted file mode 100644 index d835b86af..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png deleted file mode 100644 index 1ad01578b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png deleted file mode 100644 index 9f6074d7f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b -size 474 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png deleted file mode 100644 index c76cbf48c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90784f2523c8e7d6680cc17e618893ebf040033642a3cbaad73918c2f5d6b2f8 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png deleted file mode 100644 index b9ac5f192..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d6392bded60931b04bd5044a2e789405506bb8c98f4ac271e04af7698696c929 -size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png deleted file mode 100644 index fdb8fff15..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d065e13c7ffde92b22a15a626842a92f39b65abcbf3a4c3561cd7341801f906c -size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png deleted file mode 100644 index 50d1ae9ce..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:391ac2e3a2f949744cbafd394b7fac092ca24a82dbc80868496089a77348c4a2 -size 5455 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png deleted file mode 100644 index 0c25ee2b2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:645a37b9808db4273c7680a9543b664172a2a46700c72a4836489354db7a10c4 -size 3667 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png index 0c25ee2b2..b5915fcba 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:645a37b9808db4273c7680a9543b664172a2a46700c72a4836489354db7a10c4 -size 3667 +oid sha256:7c112653620f65900de5796db2369b64165549940bbf3d8bbc978baa26fe3d09 +size 1643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png deleted file mode 100644 index fb153d398..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80d0f8a96b226d1a5034baa32ce5cbf14ef99a7a6ae7b75907c0271698a3a749 -size 1370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png deleted file mode 100644 index 516e2f405..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c -size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png deleted file mode 100644 index 30c3e9349..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a0ded646cdc61bfff585b39f1beaa3b444e25c9866474fff335fc1b828526ac -size 2209 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png deleted file mode 100644 index 82c778d4a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb -size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png deleted file mode 100644 index 23b687e57..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af2824eed2bb429f76270556cbb939a7f32558fa5eb9d7ada891ab3c888f45b2 -size 10237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png deleted file mode 100644 index 5916a439a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:219f9ce0010b283867e39ca551b6c39a69a8cc8d35790634d554d064c6dcaaca -size 1549 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png deleted file mode 100644 index 25a223a26..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 -size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png deleted file mode 100644 index 58d7de17b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f184d9b980d6df10fc55706bf2125c95f2663969cdfd586041f6f987ac90127 -size 15195 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png deleted file mode 100644 index 19121c6a7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3dfcbf2edcbd4dbf44243bd18ff3246440e8b0739f05d0d4b3c2c8117a28d4f0 -size 15482 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png deleted file mode 100644 index 883df5636..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 -size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png deleted file mode 100644 index 55a946401..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 -size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png deleted file mode 100644 index 883df5636..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 -size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png deleted file mode 100644 index ced935c37..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 -size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png deleted file mode 100644 index 4a8a5c6e5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_CPURegion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3154bc52e2942fef0578166087ef4a7cd2b8d3bbff3a9eb2a75ca216143e860 -size 12952 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png new file mode 100644 index 000000000..27ae8e895 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60b22409f9dba870c52f04b39ad0051bb4397c00321fe935431558243d4812b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..27ae8e895 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60b22409f9dba870c52f04b39ad0051bb4397c00321fe935431558243d4812b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png new file mode 100644 index 000000000..f034e374b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8ce1ab20efc7f8ab17b614e548a7169f1070a8b46b273e10ae0cd51b13f66b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..f034e374b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8ce1ab20efc7f8ab17b614e548a7169f1070a8b46b273e10ae0cd51b13f66b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png new file mode 100644 index 000000000..15e58f4c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06c54b58d35e349cfd18aeaff9614ce5348e696ebd3da5f330b03a6d0a4ab4f +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png new file mode 100644 index 000000000..15e58f4c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06c54b58d35e349cfd18aeaff9614ce5348e696ebd3da5f330b03a6d0a4ab4f +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png new file mode 100644 index 000000000..fbecf9946 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1e8240f4bf43453704c3a7ac27b1f21174097048a94793c6bd900899175963 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png new file mode 100644 index 000000000..fbecf9946 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1e8240f4bf43453704c3a7ac27b1f21174097048a94793c6bd900899175963 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png new file mode 100644 index 000000000..c8c5208e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528d775147faaf132df52f78b80356f3df98ef519779ade404a2d6f819d265cc +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c8c5208e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528d775147faaf132df52f78b80356f3df98ef519779ade404a2d6f819d265cc +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png new file mode 100644 index 000000000..b4f2bc648 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74fc36ceebd139d08347771b75dd525bc7bb50fa78ae66cfc0480d25bc647a2 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b4f2bc648 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74fc36ceebd139d08347771b75dd525bc7bb50fa78ae66cfc0480d25bc647a2 +size 107 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index b4548df44..29685ad13 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7f7aa753d9d88fe0506a9e5f871ec6a1c734585e5591216cfd432de13eb89e8 -size 240 +oid sha256:35060ebe93aba15a6a0982ded5a13e6d3b01bc3d22a6cf65498d775aff721c88 +size 262 From 3c351a93c8bb7ef32d2bed9f007de56f2a83c32b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 22:11:32 +1000 Subject: [PATCH 127/136] Remove reference codecs and simplify tests --- tests/Directory.Build.targets | 5 - .../ImageSharp.Drawing.Tests.csproj | 3 - .../ReferenceCodecs/MagickReferenceDecoder.cs | 144 --------------- .../ReferenceCodecUtilities.cs | 94 ---------- .../ReferenceCodecs/SystemDrawingBridge.cs | 171 ------------------ .../SystemDrawingReferenceDecoder.cs | 64 ------- .../SystemDrawingReferenceEncoder.cs | 41 ----- .../TestUtilities/TestEnvironment.Formats.cs | 48 +---- .../Tests/TestEnvironmentTests.cs | 51 +----- 9 files changed, 5 insertions(+), 616 deletions(-) delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 1f2a992f7..5f5c62e53 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,11 +19,6 @@ - - diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index da30dc625..7a587c277 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -24,12 +24,9 @@ - - - diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs deleted file mode 100644 index f1e9a7cbc..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using ImageMagick; -using ImageMagick.Formats; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class MagickReferenceDecoder : ImageDecoder -{ - private readonly IImageFormat imageFormat; - private readonly bool validate; - - public MagickReferenceDecoder(IImageFormat imageFormat) - : this(imageFormat, true) - { - } - - public MagickReferenceDecoder(IImageFormat imageFormat, bool validate) - { - this.imageFormat = imageFormat; - this.validate = validate; - } - - public static MagickReferenceDecoder Png { get; } = new(PngFormat.Instance); - - public static MagickReferenceDecoder Bmp { get; } = new(BmpFormat.Instance); - - public static MagickReferenceDecoder Jpeg { get; } = new(JpegFormat.Instance); - - public static MagickReferenceDecoder Tiff { get; } = new(TiffFormat.Instance); - - public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance); - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - ImageMetadata metadata = new(); - - Configuration configuration = options.Configuration; - BmpReadDefines bmpReadDefines = new() - { - IgnoreFileSize = !this.validate - }; - PngReadDefines pngReadDefines = new() - { - IgnoreCrc = !this.validate - }; - - MagickReadSettings settings = new() - { - FrameCount = (int)options.MaxFrames - }; - settings.SetDefines(bmpReadDefines); - settings.SetDefines(pngReadDefines); - - using MagickImageCollection magickImageCollection = new(stream, settings); - List> framesList = []; - foreach (IMagickImage magicFrame in magickImageCollection) - { - ImageFrame frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height); - framesList.Add(frame); - - MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; - - using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); - if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - FromRgba32Bytes(configuration, data, framePixels); - } - else if (magicFrame.Depth is 16 or 14) - { - if (this.imageFormat is PngFormat png) - { - metadata.GetPngMetadata().BitDepth = PngBitDepth.Bit16; - } - - ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); - Span bytes = MemoryMarshal.Cast(data.AsSpan()); - FromRgba64Bytes(configuration, bytes, framePixels); - } - else - { - throw new InvalidOperationException(); - } - } - - return ReferenceCodecUtilities.EnsureDecodedMetadata(new Image(configuration, metadata, framesList), this.imageFormat); - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); - - protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using Image image = this.Decode(options, stream, cancellationToken); - ImageMetadata metadata = image.Metadata; - return new ImageInfo(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) - { - PixelType = metadata.GetDecodedPixelTypeInfo() - }; - } - - private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - Span sourcePixels = MemoryMarshal.Cast(rgbaBytes); - foreach (Memory m in destinationGroup) - { - Span destBuffer = m.Span; - PixelOperations.Instance.FromRgba32( - configuration, - sourcePixels[..destBuffer.Length], - destBuffer); - sourcePixels = sourcePixels[destBuffer.Length..]; - } - } - - private static void FromRgba64Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - foreach (Memory m in destinationGroup) - { - Span destBuffer = m.Span; - PixelOperations.Instance.FromRgba64Bytes( - configuration, - rgbaBytes, - destBuffer, - destBuffer.Length); - rgbaBytes = rgbaBytes[(destBuffer.Length * 8)..]; - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs deleted file mode 100644 index a4df368b9..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Pbm; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Qoi; -using SixLabors.ImageSharp.Formats.Tga; -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -internal static class ReferenceCodecUtilities -{ - /// - /// Ensures that the metadata is properly initialized for reference and test encoders which cannot initialize - /// metadata in the same manner as our built in decoders. - /// - /// The type of pixel format. - /// The decoded image. - /// The image format - /// The format is unknown. - public static Image EnsureDecodedMetadata(Image image, IImageFormat format) - where TPixel : unmanaged, IPixel - { - if (image.Metadata.DecodedImageFormat is null) - { - image.Metadata.DecodedImageFormat = format; - } - - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.DecodedImageFormat = format; - } - - switch (format) - { - case BmpFormat: - image.Metadata.GetBmpMetadata(); - break; - case GifFormat: - image.Metadata.GetGifMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetGifMetadata(); - } - - break; - case JpegFormat: - image.Metadata.GetJpegMetadata(); - break; - case PbmFormat: - image.Metadata.GetPbmMetadata(); - break; - case PngFormat: - image.Metadata.GetPngMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetPngMetadata(); - } - - break; - case QoiFormat: - image.Metadata.GetQoiMetadata(); - break; - case TgaFormat: - image.Metadata.GetTgaMetadata(); - break; - case TiffFormat: - image.Metadata.GetTiffMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetTiffMetadata(); - } - - break; - case WebpFormat: - image.Metadata.GetWebpMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetWebpMetadata(); - } - - break; - } - - return image; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs deleted file mode 100644 index ab9b698f7..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Drawing; -using System.Drawing.Imaging; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -/// -/// Provides methods to convert to/from System.Drawing bitmaps. -/// -public static class SystemDrawingBridge -{ - /// - /// Returns an image from the given System.Drawing bitmap. - /// - /// The pixel format. - /// The input bitmap. - /// Thrown if the image pixel format is not of type - internal static unsafe Image From32bppArgbSystemDrawingBitmap(Bitmap bmp) - where TPixel : unmanaged, IPixel - { - int w = bmp.Width; - int h = bmp.Height; - - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - - if (bmp.PixelFormat != PixelFormat.Format32bppArgb) - { - throw new ArgumentException( - $"{nameof(From32bppArgbSystemDrawingBitmap)} : pixel format should be {PixelFormat.Format32bppArgb}!", - nameof(bmp)); - } - - BitmapData data = bmp.LockBits(fullRect, ImageLockMode.ReadWrite, bmp.PixelFormat); - Image image = new(w, h); - try - { - byte* sourcePtrBase = (byte*)data.Scan0; - - long sourceRowByteCount = data.Stride; - long destRowByteCount = w * sizeof(Bgra32); - - Configuration configuration = image.Configuration; - image.ProcessPixelRows(accessor => - { - using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); - fixed (Bgra32* destPtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = accessor.GetRowSpan(y); - - byte* sourcePtr = sourcePtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - PixelOperations.Instance.FromBgra32( - configuration, - workBuffer.GetSpan().Slice(0, w), - row); - } - } - }); - } - finally - { - bmp.UnlockBits(data); - } - - return image; - } - - /// - /// Returns an image from the given System.Drawing bitmap. - /// - /// The pixel format. - /// The input bitmap. - /// Thrown if the image pixel format is not of type - internal static unsafe Image From24bppRgbSystemDrawingBitmap(Bitmap bmp) - where TPixel : unmanaged, IPixel - { - int w = bmp.Width; - int h = bmp.Height; - - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - - if (bmp.PixelFormat != PixelFormat.Format24bppRgb) - { - throw new ArgumentException( - $"{nameof(From24bppRgbSystemDrawingBitmap)}: pixel format should be {PixelFormat.Format24bppRgb}!", - nameof(bmp)); - } - - BitmapData data = bmp.LockBits(fullRect, ImageLockMode.ReadWrite, bmp.PixelFormat); - Image image = new(w, h); - try - { - byte* sourcePtrBase = (byte*)data.Scan0; - - long sourceRowByteCount = data.Stride; - long destRowByteCount = w * sizeof(Bgr24); - - Configuration configuration = image.Configuration; - Buffer2D imageBuffer = image.GetRootFramePixelBuffer(); - - using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); - fixed (Bgr24* destPtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = imageBuffer.DangerousGetRowSpan(y); - - byte* sourcePtr = sourcePtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - PixelOperations.Instance.FromBgr24(configuration, workBuffer.GetSpan().Slice(0, w), row); - } - } - } - finally - { - bmp.UnlockBits(data); - } - - return image; - } - - internal static unsafe Bitmap To32bppArgbSystemDrawingBitmap(Image image) - where TPixel : unmanaged, IPixel - { - Configuration configuration = image.Configuration; - int w = image.Width; - int h = image.Height; - - Bitmap resultBitmap = new(w, h, PixelFormat.Format32bppArgb); - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - BitmapData data = resultBitmap.LockBits(fullRect, ImageLockMode.ReadWrite, resultBitmap.PixelFormat); - try - { - byte* destPtrBase = (byte*)data.Scan0; - - long destRowByteCount = data.Stride; - long sourceRowByteCount = w * sizeof(Bgra32); - image.ProcessPixelRows(accessor => - { - using IMemoryOwner workBuffer = image.Configuration.MemoryAllocator.Allocate(w); - fixed (Bgra32* sourcePtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = accessor.GetRowSpan(y); - PixelOperations.Instance.ToBgra32(configuration, row, workBuffer.GetSpan()); - byte* destPtr = destPtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - } - } - }); - } - finally - { - resultBitmap.UnlockBits(data); - } - - return resultBitmap; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs deleted file mode 100644 index afb0d38f2..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#pragma warning disable CA1416 // Validate platform compatibility -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; -using SDBitmap = System.Drawing.Bitmap; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class SystemDrawingReferenceDecoder : ImageDecoder -{ - private readonly IImageFormat imageFormat; - - public SystemDrawingReferenceDecoder(IImageFormat imageFormat) - => this.imageFormat = imageFormat; - - public static SystemDrawingReferenceDecoder Png { get; } = new(PngFormat.Instance); - - public static SystemDrawingReferenceDecoder Bmp { get; } = new(BmpFormat.Instance); - - protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using Image image = this.Decode(options, stream, cancellationToken); - ImageMetadata metadata = image.Metadata; - return new ImageInfo(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) - { - PixelType = metadata.GetDecodedPixelTypeInfo() - }; - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using SDBitmap sourceBitmap = new(stream); - if (sourceBitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb) - { - return SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(sourceBitmap); - } - - using SDBitmap convertedBitmap = new( - sourceBitmap.Width, - sourceBitmap.Height, - System.Drawing.Imaging.PixelFormat.Format32bppArgb); - using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(convertedBitmap)) - { - g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; - g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; - g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; - - g.DrawImage(sourceBitmap, 0, 0, sourceBitmap.Width, sourceBitmap.Height); - } - - return ReferenceCodecUtilities.EnsureDecodedMetadata( - SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(convertedBitmap), - this.imageFormat); - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs deleted file mode 100644 index b3cf88c96..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Drawing; -using System.Drawing.Imaging; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class SystemDrawingReferenceEncoder : IImageEncoder -{ - private readonly ImageFormat imageFormat; - - public SystemDrawingReferenceEncoder(ImageFormat imageFormat) - => this.imageFormat = imageFormat; - - public static SystemDrawingReferenceEncoder Png { get; } = new(ImageFormat.Png); - - public static SystemDrawingReferenceEncoder Bmp { get; } = new(ImageFormat.Bmp); - - public bool SkipMetadata { get; init; } - - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - using Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image); - sdBitmap.Save(stream, this.imageFormat); - } - - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - using (Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) - { - sdBitmap.Save(stream, this.imageFormat); - } - - return Task.CompletedTask; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs index 01598903b..515732a68 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -1,22 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Tga; using IOPath = System.IO.Path; namespace SixLabors.ImageSharp.Drawing.Tests; public static partial class TestEnvironment { - private static readonly Lazy ConfigurationLazy = new(CreateDefaultConfiguration); - - internal static Configuration Configuration => ConfigurationLazy.Value; + internal static Configuration Configuration => Configuration.Default; internal static IImageDecoder GetReferenceDecoder(string filePath) { @@ -38,42 +30,4 @@ internal static IImageFormat GetImageFormat(string filePath) return format; } - - private static void ConfigureCodecs( - this Configuration cfg, - IImageFormat imageFormat, - IImageDecoder decoder, - IImageEncoder encoder, - IImageFormatDetector detector) - { - cfg.ImageFormatsManager.SetDecoder(imageFormat, decoder); - cfg.ImageFormatsManager.SetEncoder(imageFormat, encoder); - cfg.ImageFormatsManager.AddImageFormatDetector(detector); - } - - private static Configuration CreateDefaultConfiguration() - { - Configuration cfg = new( - new JpegConfigurationModule(), - new GifConfigurationModule(), - new TgaConfigurationModule()); - - // Magick codecs should work on all platforms - IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new PngEncoder(); - IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); - - cfg.ConfigureCodecs( - PngFormat.Instance, - MagickReferenceDecoder.Png, - pngEncoder, - new PngImageFormatDetector()); - - cfg.ConfigureCodecs( - BmpFormat.Instance, - IsWindows ? SystemDrawingReferenceDecoder.Bmp : MagickReferenceDecoder.Bmp, - bmpEncoder, - new BmpImageFormatDetector()); - - return cfg; - } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs index 8aa1f6a8a..e1c7cf166 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; @@ -48,66 +47,24 @@ public void GetReferenceOutputFileName() Assert.Contains(TestEnvironment.ReferenceOutputDirectoryFullPath, expected); } - [Theory] - [InlineData("lol/foo.png", typeof(SystemDrawingReferenceEncoder))] - [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceEncoder))] - [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] - [InlineData("lol/Baz.gif", typeof(GifEncoder))] - public void GetReferenceEncoder_ReturnsCorrectEncoders_Windows(string fileName, Type expectedEncoderType) - { - if (!TestEnvironment.IsWindows) - { - return; - } - - IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); - Assert.IsType(expectedEncoderType, encoder); - } - - [Theory] - [InlineData("lol/foo.png", typeof(MagickReferenceDecoder))] - [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceDecoder))] - [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] - [InlineData("lol/Baz.gif", typeof(GifDecoder))] - public void GetReferenceDecoder_ReturnsCorrectDecoders_Windows(string fileName, Type expectedDecoderType) - { - if (!TestEnvironment.IsWindows) - { - return; - } - - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); - Assert.IsType(expectedDecoderType, decoder); - } - [Theory] [InlineData("lol/foo.png", typeof(PngEncoder))] [InlineData("lol/Rofl.bmp", typeof(BmpEncoder))] [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] [InlineData("lol/Baz.gif", typeof(GifEncoder))] - public void GetReferenceEncoder_ReturnsCorrectEncoders_Linux(string fileName, Type expectedEncoderType) + public void GetReferenceEncoder_ReturnsCorrectEncoders(string fileName, Type expectedEncoderType) { - if (!TestEnvironment.IsLinux) - { - return; - } - IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); Assert.IsType(expectedEncoderType, encoder); } [Theory] - [InlineData("lol/foo.png", typeof(MagickReferenceDecoder))] - [InlineData("lol/Rofl.bmp", typeof(MagickReferenceDecoder))] + [InlineData("lol/foo.png", typeof(PngDecoder))] + [InlineData("lol/Rofl.bmp", typeof(BmpDecoder))] [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] [InlineData("lol/Baz.gif", typeof(GifDecoder))] - public void GetReferenceDecoder_ReturnsCorrectDecoders_Linux(string fileName, Type expectedDecoderType) + public void GetReferenceDecoder_ReturnsCorrectDecoders(string fileName, Type expectedDecoderType) { - if (!TestEnvironment.IsLinux) - { - return; - } - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); Assert.IsType(expectedDecoderType, decoder); } From 355b56cc38b22aa89db3e61d9b4a61debca06597 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 22:48:33 +1000 Subject: [PATCH 128/136] Add pre-flattened paths and FlattenAndTransform helper --- samples/WebGPUWindowDemo/Program.cs | 2 +- .../WebGPUDrawingBackend.cs | 10 + .../DrawingCanvas.PreFlattenedPath.cs | 213 ++++++++++++++++++ .../Processing/DrawingCanvas{TPixel}.cs | 107 ++++++++- 4 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index 0f8653fed..9997e5f5d 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -324,7 +324,7 @@ private static void OnRender(double deltaTime) fpsElapsed += deltaTime; if (fpsElapsed >= 1.0) { - window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS"; + window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS | GPU: {backend.DiagnosticGpuCompositeCount} Fallback: {backend.DiagnosticFallbackCompositeCount}"; frameCount = 0; fpsElapsed = 0; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index d52b4165d..5e749042a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -135,6 +135,16 @@ private enum PreparedBrushType : uint /// internal int TestingComputePathBatchCount { get; private set; } + /// + /// Gets the cumulative number of composition commands executed on the GPU. + /// + public int DiagnosticGpuCompositeCount => this.TestingGPUCompositeCoverageCallCount; + + /// + /// Gets the cumulative number of composition commands that fell back to the CPU backend. + /// + public int DiagnosticFallbackCompositeCount => this.TestingFallbackCompositeCoverageCallCount; + /// /// Gets a value indicating whether WebGPU is available on the current system. /// This probes the runtime by attempting to acquire an adapter and device. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs new file mode 100644 index 000000000..4d46a2f6b --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs @@ -0,0 +1,213 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Contains private pre-flattened path types used by the canvas to avoid redundant curve subdivision. +/// +public sealed partial class DrawingCanvas +{ + /// + /// A lightweight wrapper around pre-flattened points. + /// returns this directly, avoiding redundant curve subdivision. + /// Points are mutated in place on ; no buffers are copied. + /// + private sealed class PreFlattenedPath : IPath, ISimplePath + { + private readonly PointF[] points; + private readonly bool isClosed; + private RectangleF bounds; + + public PreFlattenedPath(PointF[] points, bool isClosed, RectangleF bounds) + { + this.points = points; + this.isClosed = isClosed; + this.bounds = bounds; + } + + /// + public RectangleF Bounds => this.bounds; + + /// + public PathTypes PathType => this.isClosed ? PathTypes.Closed : PathTypes.Open; + + /// + bool ISimplePath.IsClosed => this.isClosed; + + /// + ReadOnlyMemory ISimplePath.Points => this.points; + + /// + public IEnumerable Flatten() + { + yield return this; + } + + /// + /// Transforms all points in place and updates the bounds. + /// This mutates the current instance — the point buffer is not copied. + /// + /// The transform matrix. + /// This instance, with points and bounds updated. + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + for (int i = 0; i < this.points.Length; i++) + { + ref PointF p = ref this.points[i]; + p = PointF.Transform(p, matrix); + + if (p.X < minX) + { + minX = p.X; + } + + if (p.Y < minY) + { + minY = p.Y; + } + + if (p.X > maxX) + { + maxX = p.X; + } + + if (p.Y > maxY) + { + maxY = p.Y; + } + } + + this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + return this; + } + + /// + public IPath AsClosedPath() + => this.isClosed ? this : new PreFlattenedPath(this.points, true, this.bounds); + } + + /// + /// A lightweight wrapper around multiple pre-flattened sub-paths. + /// yields each sub-path directly, avoiding redundant curve subdivision. + /// Sub-path points are mutated in place on ; no buffers are copied. + /// + private sealed class PreFlattenedCompositePath : IPath + { + private readonly PreFlattenedPath[] subPaths; + private RectangleF bounds; + + public PreFlattenedCompositePath(PreFlattenedPath[] subPaths, RectangleF bounds) + { + this.subPaths = subPaths; + this.bounds = bounds; + } + + /// + public RectangleF Bounds => this.bounds; + + /// + public PathTypes PathType + { + get + { + bool hasOpen = false; + bool hasClosed = false; + foreach (PreFlattenedPath sp in this.subPaths) + { + if (sp.PathType == PathTypes.Open) + { + hasOpen = true; + } + else + { + hasClosed = true; + } + + if (hasOpen && hasClosed) + { + return PathTypes.Mixed; + } + } + + return hasClosed ? PathTypes.Closed : PathTypes.Open; + } + } + + /// + public IEnumerable Flatten() => this.subPaths; + + /// + /// Transforms all sub-path points in place and updates the bounds. + /// This mutates the current instance — no buffers are copied. + /// + /// The transform matrix. + /// This instance, with all sub-paths and bounds updated. + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + for (int i = 0; i < this.subPaths.Length; i++) + { + this.subPaths[i].Transform(matrix); + RectangleF spBounds = this.subPaths[i].Bounds; + + if (spBounds.Left < minX) + { + minX = spBounds.Left; + } + + if (spBounds.Top < minY) + { + minY = spBounds.Top; + } + + if (spBounds.Right > maxX) + { + maxX = spBounds.Right; + } + + if (spBounds.Bottom > maxY) + { + maxY = spBounds.Bottom; + } + } + + this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + return this; + } + + /// + public IPath AsClosedPath() + { + if (this.PathType == PathTypes.Closed) + { + return this; + } + + PreFlattenedPath[] closed = new PreFlattenedPath[this.subPaths.Length]; + for (int i = 0; i < this.subPaths.Length; i++) + { + closed[i] = (PreFlattenedPath)this.subPaths[i].AsClosedPath(); + } + + return new PreFlattenedCompositePath(closed, this.bounds); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 471842bc6..08c35f533 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// A drawing canvas over a frame target. /// /// The pixel format. -public sealed class DrawingCanvas : IDrawingCanvas +public sealed partial class DrawingCanvas : IDrawingCanvas where TPixel : unmanaged, IPixel { /// @@ -432,7 +432,7 @@ public void Fill(Brush brush, IPath path) IPath effectivePath = closed; if (effectiveOptions.Transform != Matrix4x4.Identity) { - effectivePath = closed.Transform(effectiveOptions.Transform); + effectivePath = FlattenAndTransform(closed, effectiveOptions.Transform); effectiveBrush = brush.Transform(effectiveOptions.Transform); } @@ -469,7 +469,7 @@ public void Process(IPath path, Action operation) IPath closed = path.AsClosedPath(); IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity ? closed - : closed.Transform(effectiveOptions.Transform); + : FlattenAndTransform(closed, effectiveOptions.Transform); transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths); Rectangle sourceRect = ToConservativeBounds(transformedPath.Bounds); @@ -552,7 +552,7 @@ public void Draw(Pen pen, IPath path) IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity ? path - : path.Transform(effectiveOptions.Transform); + : FlattenAndTransform(path, effectiveOptions.Transform); // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) @@ -1156,6 +1156,105 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList? sourceImage) => this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage); + /// + /// Flattens the path first (reusing any cached curve subdivision), then transforms + /// the resulting flat points. This avoids discarding cached + /// subdivision data that would throw away. + /// + /// + /// Flattens a path into linear segments, then transforms the resulting points in place. + /// This avoids redundant curve subdivision that would occur if we transformed the original + /// path first (which discards cached flattening) and then flattened again. + /// + /// The path to flatten and transform. The original path is not mutated. + /// The transform matrix to apply to the flattened points. + /// + /// A pre-flattened whose points are already transformed. + /// The returned path owns its point buffers and may mutate them on subsequent transforms. + /// + private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix) + { + List? subPaths = null; + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + foreach (ISimplePath sp in path.Flatten()) + { + ReadOnlySpan srcPoints = sp.Points.Span; + if (srcPoints.Length < 2) + { + continue; + } + + PointF[] dstPoints = new PointF[srcPoints.Length]; + float spMinX = float.MaxValue, spMinY = float.MaxValue; + float spMaxX = float.MinValue, spMaxY = float.MinValue; + + for (int i = 0; i < srcPoints.Length; i++) + { + ref PointF dst = ref dstPoints[i]; + dst = PointF.Transform(srcPoints[i], matrix); + + if (dst.X < spMinX) + { + spMinX = dst.X; + } + + if (dst.Y < spMinY) + { + spMinY = dst.Y; + } + + if (dst.X > spMaxX) + { + spMaxX = dst.X; + } + + if (dst.Y > spMaxY) + { + spMaxY = dst.Y; + } + } + + RectangleF spBounds = new(spMinX, spMinY, spMaxX - spMinX, spMaxY - spMinY); + subPaths ??= []; + subPaths.Add(new PreFlattenedPath(dstPoints, sp.IsClosed, spBounds)); + + if (spMinX < minX) + { + minX = spMinX; + } + + if (spMinY < minY) + { + minY = spMinY; + } + + if (spMaxX > maxX) + { + maxX = spMaxX; + } + + if (spMaxY > maxY) + { + maxY = spMaxY; + } + } + + if (subPaths is null) + { + return Path.Empty; + } + + if (subPaths.Count == 1) + { + return subPaths[0]; + } + + RectangleF totalBounds = new(minX, minY, maxX - minX, maxY - minY); + return new PreFlattenedCompositePath(subPaths.ToArray(), totalBounds); + } + /// /// Applies all clip paths to a subject path using the provided shape options. /// From 819bc1857bb6df8177eca134f1a6e255d4121c7b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 10 Mar 2026 23:00:26 +1000 Subject: [PATCH 129/136] Optimize Bezier subdivision and list allocation --- .../CubicBezierLineSegment.cs | 53 ++++++------------- src/ImageSharp.Drawing/InternalPath.cs | 9 +++- .../Backends/WebGPUDrawingBackendTests.cs | 50 +++++------------ ...rm_StarWarsCrawl_StarWarsCrawl_Default.png | 4 +- ...awl_StarWarsCrawl_WebGPU_NativeSurface.png | 4 +- 5 files changed, 40 insertions(+), 80 deletions(-) diff --git a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs index fbfe86662..214a0019e 100644 --- a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -109,48 +109,33 @@ private static PointF[] GetDrawingPoints(PointF[] controlPoints) for (int curveIndex = 0; curveIndex < curveCount; curveIndex++) { - List bezierCurveDrawingPoints = FindDrawingPoints(curveIndex, controlPoints); - - if (curveIndex != 0) + if (curveIndex == 0) { - // remove the fist point, as it coincides with the last point of the previous Bezier curve. - bezierCurveDrawingPoints.RemoveAt(0); + drawingPoints.Add(CalculateBezierPoint(curveIndex, 0, controlPoints)); } - drawingPoints.AddRange(bezierCurveDrawingPoints); + SubdivideAndAppend(curveIndex, 0, 1, controlPoints, drawingPoints, 0); + drawingPoints.Add(CalculateBezierPoint(curveIndex, 1, controlPoints)); } return [.. drawingPoints]; } - private static List FindDrawingPoints(int curveIndex, PointF[] controlPoints) - { - List pointList = []; - - Vector2 left = CalculateBezierPoint(curveIndex, 0, controlPoints); - Vector2 right = CalculateBezierPoint(curveIndex, 1, controlPoints); - - pointList.Add(left); - pointList.Add(right); - - FindDrawingPoints(curveIndex, 0, 1, pointList, 1, controlPoints, 0); - - return pointList; - } - - private static int FindDrawingPoints( + /// + /// Recursively subdivides a cubic bezier curve segment and appends points in left-to-right order. + /// Points are appended (not inserted), avoiding O(n) shifts per point. + /// + private static void SubdivideAndAppend( int curveIndex, float t0, float t1, - List pointList, - int insertionIndex, PointF[] controlPoints, + List output, int depth) { - // max recursive depth for control points, means this is approx the max number of points discoverable if (depth > 999) { - return 0; + return; } Vector2 left = CalculateBezierPoint(curveIndex, t0, controlPoints); @@ -158,7 +143,7 @@ private static int FindDrawingPoints( if ((left - right).LengthSquared() < MinimumSqrDistance) { - return 0; + return; } float midT = (t0 + t1) / 2; @@ -169,17 +154,11 @@ private static int FindDrawingPoints( if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f) { - int pointsAddedCount = 0; - - pointsAddedCount += FindDrawingPoints(curveIndex, t0, midT, pointList, insertionIndex, controlPoints, depth + 1); - pointList.Insert(insertionIndex + pointsAddedCount, mid); - pointsAddedCount++; - pointsAddedCount += FindDrawingPoints(curveIndex, midT, t1, pointList, insertionIndex + pointsAddedCount, controlPoints, depth + 1); - - return pointsAddedCount; + // Recurse left half, emit midpoint, recurse right half — all in order. + SubdivideAndAppend(curveIndex, t0, midT, controlPoints, output, depth + 1); + output.Add(mid); + SubdivideAndAppend(curveIndex, midT, t1, controlPoints, output, depth + 1); } - - return 0; } private static PointF CalculateBezierPoint(int curveIndex, float t, PointF[] controlPoints) diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs index ec142b91f..b4c4c8bee 100644 --- a/src/ImageSharp.Drawing/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -223,7 +223,14 @@ private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vecto /// private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) { - List simplified = new(segments.Count); + // Pre-compute capacity from cached flattened lengths to avoid List resizing. + int totalPoints = 0; + for (int s = 0; s < segments.Count; s++) + { + totalPoints += segments[s].Flatten().Length; + } + + List simplified = new(totalPoints); // Track indices where collinear direction reversals represent user-intended // geometry: interior points of multi-point linear segments, and junction diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index c20feca84..4f8863522 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -882,7 +882,6 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -940,7 +939,6 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvi using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -985,7 +983,6 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1026,7 +1023,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1039,7 +1035,6 @@ void DrawAction(DrawingCanvas canvas) DebugSaveBackendPair(provider, "FillPath_MultipleSeparate", defaultImage, nativeSurfaceImage); - AssertCoverageExecutionAccounting(nativeSurfaceBackend); AssertGpuPathWhenRequired(nativeSurfaceBackend); AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); @@ -1088,7 +1083,6 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1126,7 +1120,6 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1262,7 +1255,6 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1303,7 +1295,6 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1343,7 +1334,6 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1385,7 +1375,6 @@ public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1426,7 +1415,6 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1469,7 +1457,6 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1510,7 +1497,6 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1551,7 +1537,6 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProv using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1586,7 +1571,6 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(Test using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1621,7 +1605,6 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1662,7 +1645,6 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1704,7 +1686,6 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1777,10 +1758,10 @@ their first victory against Rectangle sternBounds = new(0, 0, 300, 80); Matrix4x4 sternTransform = transformBuilder .AppendQuadDistortion( - topLeft: new PointF(50, 80), - topRight: new PointF(400, 90), - bottomRight: new PointF(390, 135), - bottomLeft: new PointF(60, 140)) + topLeft: new PointF(70, 80), + topRight: new PointF(380, 90), + bottomRight: new PointF(400, 135), + bottomLeft: new PointF(50, 140)) .BuildMatrix(sternBounds); PointF[] bottomHull = @@ -1793,10 +1774,10 @@ their first victory against Rectangle hullBounds = new(0, 0, 300, 80); Matrix4x4 hullTransform = transformBuilder.Clear() .AppendQuadDistortion( - topLeft: new PointF(60, 140), - topRight: new PointF(390, 135), - bottomRight: new PointF(300, 160), - bottomLeft: new PointF(-30, 170)) + topLeft: new PointF(50, 140), + topRight: new PointF(400, 135), + bottomRight: new PointF(310, 170), + bottomLeft: new PointF(-40, 170)) .BuildMatrix(hullBounds); PointF[] towerStem = @@ -1933,7 +1914,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -1970,7 +1950,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -2005,7 +1984,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -2027,7 +2005,7 @@ public void SaveLayer_NestedLayers_MatchesDefaultOutput(TestImageProvide { DrawingOptions drawingOptions = new(); - void DrawAction(DrawingCanvas canvas) + static void DrawAction(DrawingCanvas canvas) { canvas.Fill(Brushes.Solid(Color.White)); @@ -2046,7 +2024,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -2068,7 +2045,7 @@ public void SaveLayer_WithBlendMode_MatchesDefaultOutput(TestImageProvid { DrawingOptions drawingOptions = new(); - void DrawAction(DrawingCanvas canvas) + static void DrawAction(DrawingCanvas canvas) { canvas.Fill(Brushes.Solid(Color.White)); canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(20, 20, 88, 88)); @@ -2087,7 +2064,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -2109,7 +2085,7 @@ public void SaveLayer_WithBounds_MatchesDefaultOutput(TestImageProvider< { DrawingOptions drawingOptions = new(); - void DrawAction(DrawingCanvas canvas) + static void DrawAction(DrawingCanvas canvas) { canvas.Fill(Brushes.Solid(Color.White)); @@ -2122,7 +2098,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( @@ -2144,7 +2119,7 @@ public void SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput(TestIma { DrawingOptions drawingOptions = new(); - void DrawAction(DrawingCanvas canvas) + static void DrawAction(DrawingCanvas canvas) { canvas.Fill(Brushes.Solid(Color.White)); @@ -2161,7 +2136,6 @@ void DrawAction(DrawingCanvas canvas) using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - using WebGPUDrawingBackend nativeSurfaceBackend = new(); using Image nativeSurfaceInitialImage = provider.GetImage(); using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png index b6b52d7ce..e7129410d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a862400d229f6cbbb8d609490bd82c5403c0e0eed9e366ef0b3c7df3bca8da9 -size 31777 +oid sha256:8967c06efd7f3d7cf85917969b129ee33865432793e6206711b3486cf600536e +size 32295 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png index 0ab0b108f..e56276354 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b71e2d978dd559a71c9559fbb77ac756faeb3e73c6e6beef8b35c29eaed13f23 -size 31691 +oid sha256:07d7da5068913f18909d370641348922a1b94de0ced86fadf9beeccba8576e13 +size 32134 From 9eb8dee2497eaecdba849f5bc9686bc1ca39b143 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 11 Mar 2026 00:23:31 +1000 Subject: [PATCH 130/136] Use IMemoryOwner for edge and band offset buffers --- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 74 ++++++++++++------- .../WebGPUDrawingBackend.cs | 7 +- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 6d59b46ce..6c5b08ae9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -60,7 +60,7 @@ private bool TryCreateEdgeBuffer( Configuration configuration, out WgpuBuffer* edgeBuffer, out nuint edgeBufferSize, - out EdgePlacement[] edgePlacements, + out IMemoryOwner edgePlacements, out int totalEdgeCount, out int totalBandOffsetEntries, out WgpuBuffer* bandOffsetsBuffer, @@ -71,7 +71,7 @@ private bool TryCreateEdgeBuffer( { edgeBuffer = null; edgeBufferSize = 0; - edgePlacements = []; + edgePlacements = null!; totalEdgeCount = 0; totalBandOffsetEntries = 0; bandOffsetsBuffer = null; @@ -83,7 +83,8 @@ private bool TryCreateEdgeBuffer( return true; } - edgePlacements = new EdgePlacement[definitions.Count]; + edgePlacements = flushContext.MemoryAllocator.Allocate(definitions.Count); + Span edgePlacementsSpan = edgePlacements.Memory.Span; int runningEdgeStart = 0; int runningBandOffset = 0; @@ -111,7 +112,7 @@ private bool TryCreateEdgeBuffer( IMemoryOwner? defEdgeOwner; int edgeCount; - uint[]? defBandOffsets; + IMemoryOwner? defBandOffsets; bool edgeSuccess; if (definition.IsStroke) { @@ -158,7 +159,7 @@ private bool TryCreateEdgeBuffer( // Centerline edges are band-sorted. Create one StrokeExpandCommand per band // so the GPU expand shader writes outline edges into per-band output slots. // This produces band-sorted outline edges compatible with the fill rasterizer. - uint[] clBandOffsets = defBandOffsets!; + Span clBandOffsets = defBandOffsets!.Memory.Span; LineCap defLineCap = definition.StrokeOptions?.LineCap ?? LineCap.Butt; LineJoin defLineJoin = definition.StrokeOptions?.LineJoin ?? LineJoin.Bevel; int outlineEdgesPerCenterline = ComputeOutlineEdgesPerCenterline( @@ -193,7 +194,7 @@ private bool TryCreateEdgeBuffer( // Placeholder EdgePlacement — will be updated after outline space is allocated. int bandOffsetEntriesForDef = bandCount + 1; - edgePlacements[i] = new EdgePlacement( + edgePlacementsSpan[i] = new EdgePlacement( (uint)runningEdgeStart, (uint)edgeCount, fillRule, @@ -207,7 +208,7 @@ private bool TryCreateEdgeBuffer( { int bandOffsetEntriesForDef = bandCount + 1; - edgePlacements[i] = new EdgePlacement( + edgePlacementsSpan[i] = new EdgePlacement( (uint)runningEdgeStart, (uint)edgeCount, fillRule, @@ -228,7 +229,7 @@ private bool TryCreateEdgeBuffer( // resulting outline edges are band-sorted — compatible with the fill rasterizer. // outlineBandOffsetsPerDef[defIndex] stores the outline band offsets array for // stroke definitions (null for fills). Used in the merged band offsets upload. - uint[]?[] outlineBandOffsetsPerDef = new uint[]?[definitions.Count]; + IMemoryOwner?[] outlineBandOffsetsPerDef = new IMemoryOwner?[definitions.Count]; int outlineRegionStart = totalEdgeCount; if (strokeCommands is not null) { @@ -257,7 +258,8 @@ private bool TryCreateEdgeBuffer( int defOutlineStart = (int)strokeCommands[cmdCursor].OutputStart; // Build full band offsets: offsets[b] = local offset within this def's outline region. - uint[] outOffsets = new uint[defBandCount + 1]; + IMemoryOwner outOffsetsOwner = flushContext.MemoryAllocator.Allocate(defBandCount + 1); + Span outOffsets = outOffsetsOwner.Memory.Span; uint localOffset = 0; for (int b = 0; b < defBandCount; b++) { @@ -273,11 +275,11 @@ private bool TryCreateEdgeBuffer( int defOutlineTotal = (int)localOffset; outOffsets[defBandCount] = (uint)defOutlineTotal; - outlineBandOffsetsPerDef[defIndex] = outOffsets; + outlineBandOffsetsPerDef[defIndex] = outOffsetsOwner; // Update EdgePlacement to point to the outline region. - EdgePlacement oldPlacement = edgePlacements[defIndex]; - edgePlacements[defIndex] = new EdgePlacement( + EdgePlacement oldPlacement = edgePlacementsSpan[defIndex]; + edgePlacementsSpan[defIndex] = new EdgePlacement( (uint)defOutlineStart, (uint)defOutlineTotal, oldPlacement.FillRule, @@ -343,7 +345,7 @@ private bool TryCreateEdgeBuffer( out edgeBuffer, out error)) { - DisposeGeometries(geometries, mergedEdgeOwner); + DisposeGeometries(geometries, mergedEdgeOwner, outlineBandOffsetsPerDef); return false; } @@ -378,7 +380,7 @@ private bool TryCreateEdgeBuffer( if (!TryGetOrCreateCoverageBuffer(flushContext, "band-offsets", BufferUsage.Storage | BufferUsage.CopyDst, bandOffsetsBufferSize, out bandOffsetsBuffer, out error)) { - DisposeGeometries(geometries, mergedEdgeOwner); + DisposeGeometries(geometries, mergedEdgeOwner, outlineBandOffsetsPerDef); return false; } @@ -389,14 +391,14 @@ private bool TryCreateEdgeBuffer( for (int defIndex = 0; defIndex < geometries.Length; defIndex++) { ref DefinitionGeometry geometry = ref geometries[defIndex]; - EdgePlacement placement = edgePlacements[defIndex]; + EdgePlacement placement = edgePlacementsSpan[defIndex]; int bandStart = (int)placement.CsrOffsetsStart; // Use outline band offsets for stroke definitions, fill band offsets otherwise. - uint[]? outlineOffsets = outlineBandOffsetsPerDef[defIndex]; - uint[]? defOffsets = outlineOffsets ?? geometry.BandOffsets; - if (defOffsets is not null) + IMemoryOwner? defOffsetsOwner = outlineBandOffsetsPerDef[defIndex] ?? geometry.BandOffsets; + if (defOffsetsOwner is not null) { + ReadOnlySpan defOffsets = defOffsetsOwner.Memory.Span; for (int b = 0; b < defOffsets.Length; b++) { mergedOffsets[bandStart + b] = defOffsets[b]; @@ -425,7 +427,7 @@ private bool TryCreateEdgeBuffer( strokeExpandInfo = new StrokeExpandInfo(strokeCommands, totalStrokeCenterlineEdges); } - DisposeGeometries(geometries, mergedEdgeOwner); + DisposeGeometries(geometries, mergedEdgeOwner, outlineBandOffsetsPerDef); error = null; return true; @@ -546,7 +548,10 @@ private bool TryUploadDirtyCoverageRange( /// /// Disposes all edge memory owners from geometry entries and the merged owner. /// - private static void DisposeGeometries(DefinitionGeometry[] geometries, IMemoryOwner? mergedEdgeOwner) + private static void DisposeGeometries( + DefinitionGeometry[] geometries, + IMemoryOwner? mergedEdgeOwner, + IMemoryOwner?[]? outlineBandOffsets = null) { for (int i = 0; i < geometries.Length; i++) { @@ -557,9 +562,20 @@ private static void DisposeGeometries(DefinitionGeometry[] geometries, IMemoryOw } geometries[i].EdgeOwner = null; + geometries[i].BandOffsets?.Dispose(); + geometries[i].BandOffsets = null; } mergedEdgeOwner?.Dispose(); + + if (outlineBandOffsets is not null) + { + for (int i = 0; i < outlineBandOffsets.Length; i++) + { + outlineBandOffsets[i]?.Dispose(); + outlineBandOffsets[i] = null; + } + } } private void DisposeCoverageResources() @@ -586,7 +602,7 @@ private static bool TryBuildFixedPointEdges( RasterizerSamplingOrigin samplingOrigin, out IMemoryOwner? edgeOwner, out int edgeCount, - out uint[]? bandOffsets, + out IMemoryOwner? bandOffsets, out string? error) { error = null; @@ -654,7 +670,8 @@ private static bool TryBuildFixedPointEdges( } // Prefix sum → band offsets. - uint[] offsets = new uint[bandCount + 1]; + IMemoryOwner offsetsOwner = allocator.Allocate(bandCount + 1); + Span offsets = offsetsOwner.Memory.Span; uint running = 0; for (int b = 0; b < bandCount; b++) { @@ -720,7 +737,7 @@ private static bool TryBuildFixedPointEdges( edgeOwner = finalOwner; edgeCount = totalSubEdges; - bandOffsets = offsets; + bandOffsets = offsetsOwner; return true; } @@ -740,7 +757,7 @@ private static bool TryBuildStrokeEdges( int bandCount, out IMemoryOwner? edgeOwner, out int edgeCount, - out uint[]? bandOffsets, + out IMemoryOwner? bandOffsets, out string? error) { error = null; @@ -899,7 +916,8 @@ private static bool TryBuildStrokeEdges( } // Prefix sum → band offsets. - uint[] offsets = new uint[bandCount + 1]; + IMemoryOwner offsetsOwner = allocator.Allocate(bandCount + 1); + Span offsets = offsetsOwner.Memory.Span; uint running = 0; for (int b = 0; b < bandCount; b++) { @@ -932,7 +950,7 @@ private static bool TryBuildStrokeEdges( edgeOwner = finalOwner; edgeCount = totalBandEdges; - bandOffsets = offsets; + bandOffsets = offsetsOwner; return true; } @@ -1213,13 +1231,13 @@ private struct DefinitionGeometry public IMemoryOwner? EdgeOwner; public int EdgeCount; public int BandCount; - public uint[]? BandOffsets; + public IMemoryOwner? BandOffsets; public DefinitionGeometry( IMemoryOwner? edgeOwner, int edgeCount, int bandCount, - uint[]? bandOffsets) + IMemoryOwner? bandOffsets) { this.EdgeOwner = edgeOwner; this.EdgeCount = edgeCount; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 5e749042a..192e0a0f3 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -884,7 +884,7 @@ private bool TryRenderPreparedFlush( configuration, out WgpuBuffer* edgeBuffer, out nuint edgeBufferSize, - out EdgePlacement[] edgePlacements, + out IMemoryOwner edgePlacements, out _, out _, out WgpuBuffer* bandOffsetsBuffer, @@ -962,7 +962,7 @@ private bool TryDispatchPreparedCompositeCommands( List preparedBatches, int[] batchCoverageIndices, int commandCount, - EdgePlacement[] edgePlacements, + IMemoryOwner edgePlacements, WgpuBuffer* edgeBuffer, nuint edgeBufferSize, WgpuBuffer* bandOffsetsBuffer, @@ -1025,6 +1025,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out nint brushTextureViewHandle = (nint)backdropTextureView; bool hasImageTexture = false; + ReadOnlySpan edgePlacementsSpan = edgePlacements.Memory.Span; int commandIndex = 0; for (int batchIndex = 0; batchIndex < preparedBatches.Count; batchIndex++) { @@ -1167,7 +1168,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out return false; } - EdgePlacement edgePlacement = edgePlacements[coverageDefinitionIndex]; + EdgePlacement edgePlacement = edgePlacementsSpan[coverageDefinitionIndex]; Rectangle destinationRegion = command.DestinationRegion; Point sourceOffset = command.SourceOffset; From 83441110e93f0813ca8692e61c361fc249730158 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 11 Mar 2026 00:28:36 +1000 Subject: [PATCH 131/136] Cleanup --- .../Processing/DrawingCanvas{TPixel}.cs | 13 ++----------- .../Processing/RadialGradientBrush.cs | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 08c35f533..82afeee1a 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -54,12 +54,6 @@ public sealed partial class DrawingCanvas : IDrawingCanvas /// private readonly List> pendingImageResources = []; - /// - /// Indicates whether this canvas is the root owner of the target frame. - /// Only the root canvas releases backend resources on dispose. - /// - private readonly bool isRoot; - /// /// Tracks whether this instance has already been disposed. /// @@ -135,14 +129,12 @@ internal DrawingCanvas( /// The destination frame. /// The command batcher used for deferred composition. /// The default state used when no scoped state is active. - /// Whether this canvas is the root owner of the target frame. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, DrawingCanvasBatcher batcher, - DrawingCanvasState defaultState, - bool isRoot) + DrawingCanvasState defaultState) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); @@ -159,7 +151,6 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; - this.isRoot = isRoot; // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); @@ -1252,7 +1243,7 @@ private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix) } RectangleF totalBounds = new(minX, minY, maxX - minX, maxY - minY); - return new PreFlattenedCompositePath(subPaths.ToArray(), totalBounds); + return new PreFlattenedCompositePath([.. subPaths], totalBounds); } /// diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index c339134d1..5b7221c57 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Processing; - using System.Numerics; using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; +namespace SixLabors.ImageSharp.Drawing.Processing; + /// /// A radial gradient brush defined by either one circle or two circles. /// When one circle is provided, the gradient parameter is the distance from the center divided by the radius. From dc538ad4b334b1051fdbae2b2c71849334c03498 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 11 Mar 2026 00:30:07 +1000 Subject: [PATCH 132/136] Use auto-properties; drop isRoot flag --- .../Processing/DRAWING_CANVAS.md | 3 - .../Processing/DrawingCanvas{TPixel}.cs | 6 +- .../Processing/RadialGradientBrush.cs | 57 +++++++++---------- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md index 779559940..de77ba1d6 100644 --- a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md +++ b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md @@ -18,7 +18,6 @@ DrawingCanvas : IDrawingCanvas, IDisposable savedStates : Stack (min depth 1) layerDataStack : Stack> (one per active SaveLayer) pendingImageResources : List> (temp images awaiting flush) - isRoot : bool (only root releases frame resources) isDisposed : bool ``` @@ -238,7 +237,6 @@ CreateRegion(region) -> create child canvas: - shares backend and batcher with parent - snapshots current state - - isRoot = false (no resource release on dispose) - local origin is (0,0) within clipped region ``` @@ -258,7 +256,6 @@ Dispose() Phase 3: Cleanup (in finally) -> DisposePendingImageResources() - -> if isRoot: backend.ReleaseFrameResources(target) -> isDisposed = true ``` diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 82afeee1a..04f94857f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -115,8 +115,7 @@ internal DrawingCanvas( backend, targetFrame, new DrawingCanvasBatcher(configuration, backend, targetFrame), - new DrawingCanvasState(options, clipPaths), - isRoot: true) + new DrawingCanvasState(options, clipPaths)) { } @@ -350,8 +349,7 @@ public DrawingCanvas CreateRegion(Rectangle region) this.backend, childFrame, this.batcher, - this.ResolveState(), - isRoot: false); + this.ResolveState()); } /// diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 5b7221c57..e83aacf3c 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -15,11 +15,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class RadialGradientBrush : GradientBrush { - private readonly PointF center0; - private readonly float radius0; - private readonly PointF? center1; // null means single-circle form - private readonly float? radius1; - /// /// Initializes a new instance of the class using a single circle. /// @@ -34,10 +29,10 @@ public RadialGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center0 = center; - this.radius0 = radius; - this.center1 = null; - this.radius1 = null; + this.Center0 = center; + this.Radius0 = radius; + this.Center1 = null; + this.Radius1 = null; } /// @@ -58,49 +53,49 @@ public RadialGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center0 = startCenter; - this.radius0 = startRadius; - this.center1 = endCenter; - this.radius1 = endRadius; + this.Center0 = startCenter; + this.Radius0 = startRadius; + this.Center1 = endCenter; + this.Radius1 = endRadius; } /// /// Gets the center of the starting circle. /// - public PointF Center0 => this.center0; + public PointF Center0 { get; } /// /// Gets the radius of the starting circle. /// - public float Radius0 => this.radius0; + public float Radius0 { get; } /// /// Gets the center of the ending circle, or for single-circle form. /// - public PointF? Center1 => this.center1; + public PointF? Center1 { get; } /// /// Gets the radius of the ending circle, or for single-circle form. /// - public float? Radius1 => this.radius1; + public float? Radius1 { get; } /// /// Gets a value indicating whether this is a two-circle radial gradient. /// - public bool IsTwoCircle => this.center1.HasValue && this.radius1.HasValue; + public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue; /// public override Brush Transform(Matrix4x4 matrix) { - PointF tc0 = PointF.Transform(this.center0, matrix); + PointF tc0 = PointF.Transform(this.Center0, matrix); float scale = MatrixUtilities.GetAverageScale(in matrix); if (this.IsTwoCircle) { - PointF tc1 = PointF.Transform(this.center1!.Value, matrix); - return new RadialGradientBrush(tc0, this.radius0 * scale, tc1, this.radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray); + PointF tc1 = PointF.Transform(this.Center1!.Value, matrix); + return new RadialGradientBrush(tc0, this.Radius0 * scale, tc1, this.Radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray); } - return new RadialGradientBrush(tc0, this.radius0 * scale, this.RepetitionMode, this.ColorStopsArray); + return new RadialGradientBrush(tc0, this.Radius0 * scale, this.RepetitionMode, this.ColorStopsArray); } /// @@ -109,10 +104,10 @@ public override bool Equals(Brush? other) if (other is RadialGradientBrush b) { return base.Equals(other) - && this.center0.Equals(b.center0) - && this.radius0.Equals(b.radius0) - && Nullable.Equals(this.center1, b.center1) - && Nullable.Equals(this.radius1, b.radius1); + && this.Center0.Equals(b.Center0) + && this.Radius0.Equals(b.Radius0) + && Nullable.Equals(this.Center1, b.Center1) + && Nullable.Equals(this.Radius1, b.Radius1); } return false; @@ -120,7 +115,7 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), this.center0, this.radius0, this.center1, this.radius1); + => HashCode.Combine(base.GetHashCode(), this.Center0, this.Radius0, this.Center1, this.Radius1); /// public override BrushApplicator CreateApplicator( @@ -132,10 +127,10 @@ public override BrushApplicator CreateApplicator( configuration, options, targetRegion, - this.center0, - this.radius0, - this.center1, - this.radius1, + this.Center0, + this.Radius0, + this.Center1, + this.Radius1, this.ColorStopsArray, this.RepetitionMode); From fca77ba085f94f301b540c3e520a0d213470c95b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 11 Mar 2026 08:01:34 +1000 Subject: [PATCH 133/136] Add no AA rectangle tests --- .../DrawingCanvasTests.Primitives.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs index d4dde3da3..16e3e5f04 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs @@ -8,6 +8,127 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public partial class DrawingCanvasTests { + [Theory] + [WithBlankImage(50, 50, PixelTypes.Rgba32)] + public void FillRectangle_AliasedRendersFullCorners(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + const int x = 10; + const int y = 10; + const int w = 30; + const int h = 20; + + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false } + }; + + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.Black)); + canvas.Fill(Brushes.Solid(Color.White), new Rectangle(x, y, w, h)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + // Verify all four corner pixels are fully white. + Rgba32 topLeft = target[x, y].ToRgba32(); + Rgba32 topRight = target[x + w - 1, y].ToRgba32(); + Rgba32 bottomLeft = target[x, y + h - 1].ToRgba32(); + Rgba32 bottomRight = target[x + w - 1, y + h - 1].ToRgba32(); + + Assert.Equal(255, topLeft.R); + Assert.Equal(255, topRight.R); + Assert.Equal(255, bottomLeft.R); + Assert.Equal(255, bottomRight.R); + + // Verify pixels just outside each corner are still black. + Assert.Equal(0, target[x - 1, y].ToRgba32().R); + Assert.Equal(0, target[x, y - 1].ToRgba32().R); + Assert.Equal(0, target[x + w, y].ToRgba32().R); + Assert.Equal(0, target[x + w - 1, y - 1].ToRgba32().R); + Assert.Equal(0, target[x - 1, y + h - 1].ToRgba32().R); + Assert.Equal(0, target[x, y + h].ToRgba32().R); + Assert.Equal(0, target[x + w, y + h - 1].ToRgba32().R); + Assert.Equal(0, target[x + w - 1, y + h].ToRgba32().R); + + // Verify interior pixel count matches expected area. + int whiteCount = 0; + target.ProcessPixelRows(accessor => + { + for (int row = 0; row < accessor.Height; row++) + { + Span span = accessor.GetRowSpan(row); + for (int col = 0; col < span.Length; col++) + { + if (span[col].ToRgba32().R == 255) + { + whiteCount++; + } + } + } + }); + + Assert.Equal(w * h, whiteCount); + } + + [Theory] + [WithBlankImage(50, 50, PixelTypes.Rgba32)] + public void DrawRectangle_AliasedRendersFullCorners(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // A 2px pen centered on the rectangle edge places 1px inside and 1px outside. + // For a rect at (10,10) size 30x20, the outer stroke boundary is (9,9)-(40,30) + // and the inner boundary is (11,11)-(38,28). + // Miter join ensures corners are fully filled (bevel would cut them). + const int x = 10; + const int y = 10; + const int w = 30; + const int h = 20; + const float penWidth = 2; + + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.01F } + }; + + SolidPen pen = new(new PenOptions(Color.White, penWidth, null) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter } + }); + + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.Black)); + canvas.Draw(pen, new Rectangle(x, y, w, h)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + // Outer corners of the stroke (1px outside the rect edge). + Assert.Equal(255, target[x - 1, y - 1].ToRgba32().R); + Assert.Equal(255, target[x + w, y - 1].ToRgba32().R); + Assert.Equal(255, target[x - 1, y + h].ToRgba32().R); + Assert.Equal(255, target[x + w, y + h].ToRgba32().R); + + // Inner corners of the stroke (1px inside the rect edge). + Assert.Equal(255, target[x, y].ToRgba32().R); + Assert.Equal(255, target[x + w - 1, y].ToRgba32().R); + Assert.Equal(255, target[x, y + h - 1].ToRgba32().R); + Assert.Equal(255, target[x + w - 1, y + h - 1].ToRgba32().R); + + // Well outside the stroke boundary should be black. + Assert.Equal(0, target[x - 3, y - 3].ToRgba32().R); + Assert.Equal(0, target[x + w + 2, y - 3].ToRgba32().R); + Assert.Equal(0, target[x - 3, y + h + 2].ToRgba32().R); + Assert.Equal(0, target[x + w + 2, y + h + 2].ToRgba32().R); + + // Interior of the rectangle (well inside the stroke) should be black. + Assert.Equal(0, target[x + (w / 2), y + (h / 2)].ToRgba32().R); + } + [Theory] [WithBlankImage(240, 160, PixelTypes.Rgba32)] public void DrawPrimitiveHelpers_MatchesReference(TestImageProvider provider) From ba578467109f0d94e88ec512851c8d2cc56e963d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 11 Mar 2026 08:34:28 +1000 Subject: [PATCH 134/136] Remove some copying. --- .../WebGPUDrawingBackend.Readback.cs | 20 +++++++++---------- .../WebGPUDrawingBackend.cs | 17 ++++++++-------- .../Backends/DefaultDrawingBackend.cs | 15 ++++++-------- .../Processing/Backends/IDrawingBackend.cs | 13 ++++++------ .../Processing/DrawingCanvas{TPixel}.cs | 18 +++++++++++++++-- .../Processing/DrawingCanvasBatcherTests.cs | 8 ++------ .../Processing/DrawingCanvasTests.Process.cs | 14 +++++++++---- .../RasterizerDefaultsExtensionsTests.cs | 9 +++------ 8 files changed, 62 insertions(+), 52 deletions(-) diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs index 028f921b8..77d52e53c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -2,10 +2,11 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Silk.NET.WebGPU; using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using WgpuBuffer = Silk.NET.WebGPU.Buffer; @@ -23,14 +24,13 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) + Buffer2D destination) where TPixel : unmanaged, IPixel { this.ThrowIfDisposed(); Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(target, nameof(target)); - - image = null; + Guard.NotNull(destination, nameof(destination)); // Readback is only available for native WebGPU targets with valid interop handles. if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || @@ -78,7 +78,6 @@ public bool TryReadRegion( // WebGPU copy-to-buffer requires bytes-per-row alignment to 256 bytes. int readbackRowBytes = Align(packedRowBytes, 256); - int packedByteCount = checked(packedRowBytes * source.Height); ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)source.Height); WgpuBuffer* readbackBuffer = null; @@ -169,18 +168,17 @@ void Callback(BufferMapAsyncStatus status, void* userData) try { ReadOnlySpan readback = new(mapped, checked((int)readbackByteCount)); - byte[] packed = new byte[packedByteCount]; - Span packedSpan = packed; - // Strip WebGPU row padding so Image.LoadPixelData receives tightly packed rows. - for (int y = 0; y < source.Height; y++) + // Copy directly from the mapped GPU buffer into the caller's buffer, + // stripping WebGPU row padding in the process. Single copy, no intermediate array. + int copyHeight = Math.Min(source.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) { readback .Slice(y * readbackRowBytes, packedRowBytes) - .CopyTo(packedSpan.Slice(y * packedRowBytes, packedRowBytes)); + .CopyTo(MemoryMarshal.AsBytes(destination.DangerousGetRowSpan(y))); } - image = Image.LoadPixelData(configuration, packed, source.Width, source.Height); return true; } finally diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 192e0a0f3..6bfbeac61 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -675,24 +675,25 @@ private void ComposeLayerFallback( _ = destination.TryGetNativeSurface(out NativeSurface? destSurface); _ = destSurface!.TryGetCapability(out WebGPUSurfaceCapability? destCapability); - // Read destination and source from the GPU into CPU images. - if (!this.TryReadRegion(configuration, destination, destination.Bounds, out Image? destImage)) + MemoryAllocator allocator = configuration.MemoryAllocator; + + // Read destination and source from the GPU into CPU buffers. + using Buffer2D destBuffer = allocator.Allocate2D(destination.Bounds.Width, destination.Bounds.Height); + if (!this.TryReadRegion(configuration, destination, destination.Bounds, destBuffer)) { return; } - if (!this.TryReadRegion(configuration, source, source.Bounds, out Image? srcImage)) + using Buffer2D srcBuffer = allocator.Allocate2D(source.Bounds.Width, source.Bounds.Height); + if (!this.TryReadRegion(configuration, source, source.Bounds, srcBuffer)) { - destImage.Dispose(); return; } - using (destImage) - using (srcImage) { - Buffer2DRegion destRegion = new(destImage.Frames.RootFrame.PixelBuffer); + Buffer2DRegion destRegion = new(destBuffer); ICanvasFrame destFrame = new MemoryCanvasFrame(destRegion); - ICanvasFrame srcFrame = new MemoryCanvasFrame(new Buffer2DRegion(srcImage.Frames.RootFrame.PixelBuffer)); + ICanvasFrame srcFrame = new MemoryCanvasFrame(new Buffer2DRegion(srcBuffer)); this.fallbackBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options); diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index a61268a62..849a4a457 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -159,15 +158,15 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) + Buffer2D destination) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(destination, nameof(destination)); // CPU backend readback is available only when the target exposes CPU pixels. if (!target.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) { - image = null; return false; } @@ -178,17 +177,15 @@ public bool TryReadRegion( if (clipped.Width <= 0 || clipped.Height <= 0) { - image = null; return false; } - // Build a tightly packed temporary image for downstream processing operations. - image = new(configuration, clipped.Width, clipped.Height); - Buffer2D destination = image.Frames.RootFrame.PixelBuffer; - for (int y = 0; y < clipped.Height; y++) + int copyWidth = Math.Min(clipped.Width, destination.Width); + int copyHeight = Math.Min(clipped.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) { sourceRegion.DangerousGetRowSpan(clipped.Y + y) - .Slice(clipped.X, clipped.Width) + .Slice(clipped.X, copyWidth) .CopyTo(destination.DangerousGetRowSpan(y)); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 1b0dbc6aa..ce0a85340 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -29,21 +29,22 @@ public void FlushCompositions( where TPixel : unmanaged, IPixel; /// - /// Attempts to read source pixels from the target into a temporary image. + /// Attempts to read source pixels from the target into a caller-provided buffer. /// - /// The destination pixel format. + /// The pixel format. /// The active processing configuration. /// The target frame. /// Source rectangle in target-local coordinates. - /// - /// When this method returns , receives a newly allocated source image. + /// + /// The caller-allocated buffer to receive the pixel data. + /// Must be at least as large as (clamped to target bounds). /// /// when readback succeeds; otherwise . public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) + Buffer2D destination) where TPixel : unmanaged, IPixel; /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 04f94857f..7438358a8 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -240,6 +240,9 @@ public int Save() /// public int Save(DrawingOptions options, params IPath[] clipPaths) + => this.SaveCore(options, clipPaths); + + private int SaveCore(DrawingOptions options, IReadOnlyList clipPaths) { this.EnsureNotDisposed(); Guard.NotNull(options, nameof(options)); @@ -1125,7 +1128,7 @@ private void DrawTextOperations( private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) { int saveCount = this.savedStates.Count; - _ = this.Save(options, [.. clipPaths]); + _ = this.SaveCore(options, clipPaths); try { action(); @@ -1138,12 +1141,23 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList /// Attempts to create a source image for process-in-path operations. + /// The backend copies pixels directly into the image's pixel buffer — single copy. /// /// Source rectangle in local canvas coordinates. /// The readback image when available. /// when source pixels were resolved. private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image? sourceImage) - => this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage); + { + sourceImage = new Image(this.configuration, sourceRect.Width, sourceRect.Height); + if (!this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, sourceImage.Frames.RootFrame.PixelBuffer)) + { + sourceImage.Dispose(); + sourceImage = null; + return false; + } + + return true; + } /// /// Flattens the path first (reusing any cached curve subdivision), then transforms diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index cde8f18a5..21ef1d93b 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -167,12 +166,9 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image image) + Buffer2D destination) where TPixel : unmanaged, IPixel - { - image = null; - return false; - } + => false; public void ComposeLayer( Configuration configuration, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index 1994f46b7..9e4455c69 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -198,14 +198,13 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - out Image? image) + Buffer2D destination) where TTargetPixel : unmanaged, IPixel { this.LastReadbackConfiguration = configuration; if (this.readbackSource is null) { - image = null; return false; } @@ -214,12 +213,19 @@ public bool TryReadRegion( Rectangle clipped = Rectangle.Intersect(this.readbackSource.Bounds, sourceRectangle); if (clipped.Width <= 0 || clipped.Height <= 0) { - image = null; return false; } using Image cropped = this.readbackSource.Clone(ctx => ctx.Crop(clipped)); - image = cropped.CloneAs(); + using Image converted = cropped.CloneAs(); + Buffer2D source = converted.Frames.RootFrame.PixelBuffer; + int copyWidth = Math.Min(source.Width, destination.Width); + int copyHeight = Math.Min(source.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) + { + source.DangerousGetRowSpan(y).Slice(0, copyWidth).CopyTo(destination.DangerousGetRowSpan(y)); + } + return true; } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 07117d628..8ff08a310 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -59,12 +59,9 @@ public bool TryReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, - [NotNullWhen(true)] out Image? image) + Buffer2D destination) where TPixel : unmanaged, IPixel - { - image = null; - return false; - } + => false; public void ComposeLayer( Configuration configuration, From 7a0869532a029975a69bead34210e0677431502b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 12 Mar 2026 12:06:53 +1000 Subject: [PATCH 135/136] Update dependencies --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index f91d2a3f1..5eefa08b4 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -45,8 +45,8 @@ - - + + From e146a2b98ff0bdf3465a62c00c665285d2154233 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 12 Mar 2026 18:54:09 +1000 Subject: [PATCH 136/136] Refactor path flattening and remove tessellation --- ...WebGPUDrawingBackend.CoverageRasterizer.cs | 40 +++--- .../ImageSharp.Drawing.csproj | 2 +- src/ImageSharp.Drawing/InternalPath.cs | 13 -- .../Processing/Backends/CompositionCommand.cs | 3 +- .../Backends/CompositionCoverageDefinition.cs | 3 +- .../Processing/Backends/DefaultRasterizer.cs | 90 ++++-------- .../DrawingCanvas.PreFlattenedPath.cs | 45 ++++-- .../DrawingCanvasBatcher{TPixel}.cs | 7 +- .../Processing/DrawingCanvas{TPixel}.cs | 43 +++++- .../TessellatedMultipolygon.cs | 132 ------------------ .../Issues/Issue_134.cs | 24 ++-- .../TessellatedMultipolygonTests.cs | 93 ------------ ...hesReference_Rgba32_ColrV1-draw-glyphs.png | 4 +- ...atchesReference_Rgba32_Svg-draw-glyphs.png | 4 +- ...zontal_Rgba32_Blank100x100_type-spiral.png | 4 +- ...ntal_Rgba32_Blank120x120_type-triangle.png | 4 +- .../FillPathSVGArcs.png | 4 +- ...everse(True)_IntersectionRule(EvenOdd).png | 4 +- ...everse(True)_IntersectionRule(NonZero).png | 4 +- 19 files changed, 154 insertions(+), 369 deletions(-) delete mode 100644 src/ImageSharp.Drawing/TessellatedMultipolygon.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs index 6c5b08ae9..a0ae5654f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs @@ -617,20 +617,23 @@ private static bool TryBuildFixedPointEdges( int interestY = interest.Y; int bandCount = (int)DivideRoundUp(height, TileHeight); - // Pass 1: Flatten path and count edges per band. + // Flatten once and reuse the list for both passes. + IEnumerable flattened = path.Flatten(); + IReadOnlyList contours = flattened is IReadOnlyList list + ? list + : [.. flattened]; + + // Pass 1: Count edges per band. int totalSubEdges = 0; using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); Span bandCounts = bandCountsOwner.Memory.Span; - foreach (ISimplePath simplePath in path.Flatten()) + for (int c = 0; c < contours.Count; c++) { + ISimplePath simplePath = contours[c]; ReadOnlySpan points = simplePath.Points.Span; - if (points.Length < 2) - { - continue; - } - int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1; + for (int j = 0; j < segmentCount; j++) { PointF p0 = points[j]; @@ -681,20 +684,16 @@ private static bool TryBuildFixedPointEdges( offsets[bandCount] = running; - // Pass 2: Flatten again and scatter edges directly into the final buffer. + // Pass 2: Scatter edges directly into the final buffer. IMemoryOwner finalOwner = allocator.Allocate(totalSubEdges); Span finalSpan = finalOwner.Memory.Span; using IMemoryOwner writeCursorsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); Span writeCursors = writeCursorsOwner.Memory.Span; - foreach (ISimplePath simplePath in path.Flatten()) + for (int c = 0; c < contours.Count; c++) { + ISimplePath simplePath = contours[c]; ReadOnlySpan points = simplePath.Points.Span; - if (points.Length < 2) - { - continue; - } - int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1; for (int j = 0; j < segmentCount; j++) { @@ -775,17 +774,20 @@ private static bool TryBuildStrokeEdges( float halfWidth = strokeWidth * 0.5f; int yExpansionFixed = (int)MathF.Ceiling(Math.Max(miterLimit, 1f) * halfWidth * FixedOne); + // Flatten once and reuse the list. + IEnumerable flattened = path.Flatten(); + IReadOnlyList contours = flattened is IReadOnlyList list + ? list + : [.. flattened]; + // Pass 1: Collect all stroke edges and count per band. List strokeEdges = []; List<(int YMinFixed, int YMaxFixed)> edgeYRanges = []; - foreach (ISimplePath simplePath in path.Flatten()) + for (int c = 0; c < contours.Count; c++) { + ISimplePath simplePath = contours[c]; ReadOnlySpan points = simplePath.Points.Span; - if (points.Length < 2) - { - continue; - } bool isClosed = simplePath.IsClosed; int segmentCount = isClosed ? points.Length : points.Length - 1; diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 5eefa08b4..8d508564b 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -45,7 +45,7 @@ - + diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs index b4c4c8bee..e7571a5ce 100644 --- a/src/ImageSharp.Drawing/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -178,19 +178,6 @@ internal SegmentInfo PointAlongPath(float distanceAlongPath) }; } - internal IMemoryOwner ExtractVertices(MemoryAllocator allocator) - { - IMemoryOwner buffer = allocator.Allocate(this.points.Length + 1); - Span span = buffer.Memory.Span; - - for (int i = 0; i < this.points.Length; i++) - { - span[i] = this.points[i].Point; - } - - return buffer; - } - // Modulo is a very slow operation. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index dbd64044d..f952bd69a 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -53,7 +53,8 @@ private CompositionCommand( public int DefinitionKey { get; } /// - /// Gets the path to rasterize in target-local coordinates. + /// Gets the flattened path to rasterize in target-local coordinates. + /// All sub-paths are pre-flattened and oriented for correct fill rasterization. /// public IPath Path { get; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs index 882fa1d39..f3c93f390 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -58,7 +58,8 @@ public CompositionCoverageDefinition( public int DefinitionKey { get; } /// - /// Gets the path used to generate coverage. + /// Gets the closed, flattened path used to generate coverage. + /// All sub-paths are pre-flattened and oriented for correct fill rasterization. /// public IPath Path { get; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 4126a4bbc..61102e6f6 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -202,10 +202,26 @@ private static void RasterizeCoreRows( float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); - using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); + IEnumerable flattened = path.Flatten(); + IReadOnlyList contours = flattened is IReadOnlyList list + ? list + : [.. flattened]; + + int totalSegments = 0; + for (int i = 0; i < contours.Count; i++) + { + ISimplePath sp = contours[i]; + totalSegments += sp.IsClosed ? sp.Points.Length : sp.Points.Length - 1; + } + + if (totalSegments == 0) + { + return; + } + + using IMemoryOwner edgeDataOwner = allocator.Allocate(totalSegments); int edgeCount = BuildEdgeTable( - multipolygon, + contours, interest.Left, interest.Top, height, @@ -1164,7 +1180,7 @@ private static int BuildStrokeEdgeTable( /// /// Builds an edge table in scanner-local coordinates. /// - /// Input tessellated rings. + /// Flattened path contours. /// Interest left in absolute coordinates. /// Interest top in absolute coordinates. /// Interest height in pixels. @@ -1173,7 +1189,7 @@ private static int BuildStrokeEdgeTable( /// Destination span for edge records. /// Number of valid edge records written. private static int BuildEdgeTable( - TessellatedMultipolygon multipolygon, + IReadOnlyList contours, int minX, int minY, int height, @@ -1182,13 +1198,15 @@ private static int BuildEdgeTable( Span destination) { int count = 0; - foreach (TessellatedMultipolygon.Ring ring in multipolygon) + for (int r = 0; r < contours.Count; r++) { - ReadOnlySpan vertices = ring.Vertices; - for (int i = 0; i < ring.VertexCount; i++) + ISimplePath contour = contours[r]; + ReadOnlySpan points = contour.Points.Span; + int segmentCount = contour.IsClosed ? points.Length : points.Length - 1; + for (int i = 0; i < segmentCount; i++) { - PointF p0 = vertices[i]; - PointF p1 = vertices[i + 1]; + PointF p0 = points[i]; + PointF p1 = points[i + 1 == points.Length ? 0 : i + 1]; float x0 = (p0.X - minX) + samplingOffsetX; float y0 = (p0.Y - minY) + samplingOffsetY; @@ -1530,58 +1548,6 @@ public Context( this.touchedRowCount = 0; } - /// - /// Rasterizes all edges in a tessellated multipolygon directly into this context. - /// - /// Input tessellated rings. - /// Absolute left coordinate of the current scanner window. - /// Absolute top coordinate of the current scanner window. - /// Horizontal sample origin offset. - /// Vertical sample origin offset. - public void RasterizeMultipolygon( - TessellatedMultipolygon multipolygon, - int minX, - int minY, - float samplingOffsetX, - float samplingOffsetY) - { - foreach (TessellatedMultipolygon.Ring ring in multipolygon) - { - ReadOnlySpan vertices = ring.Vertices; - for (int i = 0; i < ring.VertexCount; i++) - { - PointF p0 = vertices[i]; - PointF p1 = vertices[i + 1]; - - float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = (p0.Y - minY) + samplingOffsetY; - float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = (p1.Y - minY) + samplingOffsetY; - - if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) - { - continue; - } - - if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) - { - continue; - } - - int fx0 = FloatToFixed24Dot8(x0); - int fy0 = FloatToFixed24Dot8(y0); - int fx1 = FloatToFixed24Dot8(x1); - int fy1 = FloatToFixed24Dot8(y1); - if (fy0 == fy1) - { - continue; - } - - this.RasterizeLine(fx0, fy0, fx1, fy1); - } - } - } - /// /// Rasterizes all prebuilt edges that overlap this context. /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs index 4d46a2f6b..cab98d643 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs @@ -15,13 +15,13 @@ public sealed partial class DrawingCanvas /// returns this directly, avoiding redundant curve subdivision. /// Points are mutated in place on ; no buffers are copied. /// - private sealed class PreFlattenedPath : IPath, ISimplePath + private sealed class FlattenedPath : IPath, ISimplePath { private readonly PointF[] points; private readonly bool isClosed; private RectangleF bounds; - public PreFlattenedPath(PointF[] points, bool isClosed, RectangleF bounds) + public FlattenedPath(PointF[] points, bool isClosed, RectangleF bounds) { this.points = points; this.isClosed = isClosed; @@ -94,7 +94,21 @@ public IPath Transform(Matrix4x4 matrix) /// public IPath AsClosedPath() - => this.isClosed ? this : new PreFlattenedPath(this.points, true, this.bounds); + { + if (this.isClosed) + { + return this; + } + + PointF[] closedPoints = new PointF[this.points.Length + 1]; + for (int i = 0; i < this.points.Length; i++) + { + closedPoints[i] = this.points[i]; + } + + closedPoints[^1] = this.points[0]; + return new FlattenedPath(closedPoints, true, this.bounds); + } } /// @@ -102,12 +116,13 @@ public IPath AsClosedPath() /// yields each sub-path directly, avoiding redundant curve subdivision. /// Sub-path points are mutated in place on ; no buffers are copied. /// - private sealed class PreFlattenedCompositePath : IPath + private sealed class FlattenedCompositePath : IPath { - private readonly PreFlattenedPath[] subPaths; + private readonly FlattenedPath[] subPaths; private RectangleF bounds; + private PathTypes? pathType; - public PreFlattenedCompositePath(PreFlattenedPath[] subPaths, RectangleF bounds) + public FlattenedCompositePath(FlattenedPath[] subPaths, RectangleF bounds) { this.subPaths = subPaths; this.bounds = bounds; @@ -121,9 +136,14 @@ public PathTypes PathType { get { + if (this.pathType.HasValue) + { + return this.pathType.Value; + } + bool hasOpen = false; bool hasClosed = false; - foreach (PreFlattenedPath sp in this.subPaths) + foreach (FlattenedPath sp in this.subPaths) { if (sp.PathType == PathTypes.Open) { @@ -140,7 +160,8 @@ public PathTypes PathType } } - return hasClosed ? PathTypes.Closed : PathTypes.Open; + this.pathType = hasClosed ? PathTypes.Closed : PathTypes.Open; + return this.pathType.Value; } } @@ -165,7 +186,7 @@ public IPath Transform(Matrix4x4 matrix) for (int i = 0; i < this.subPaths.Length; i++) { - this.subPaths[i].Transform(matrix); + _ = this.subPaths[i].Transform(matrix); RectangleF spBounds = this.subPaths[i].Bounds; if (spBounds.Left < minX) @@ -201,13 +222,13 @@ public IPath AsClosedPath() return this; } - PreFlattenedPath[] closed = new PreFlattenedPath[this.subPaths.Length]; + FlattenedPath[] closed = new FlattenedPath[this.subPaths.Length]; for (int i = 0; i < this.subPaths.Length; i++) { - closed[i] = (PreFlattenedPath)this.subPaths[i].AsClosedPath(); + closed[i] = (FlattenedPath)this.subPaths[i].AsClosedPath(); } - return new PreFlattenedCompositePath(closed, this.bounds); + return new FlattenedCompositePath(closed, this.bounds); } } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index 069979c47..12cc27ab6 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -19,7 +19,6 @@ internal sealed class DrawingCanvasBatcher { private readonly Configuration configuration; private readonly IDrawingBackend backend; - private readonly ICanvasFrame targetFrame; private readonly List commands = []; internal DrawingCanvasBatcher( @@ -29,13 +28,13 @@ internal DrawingCanvasBatcher( { this.configuration = configuration; this.backend = backend; - this.targetFrame = targetFrame; + this.TargetFrame = targetFrame; } /// /// Gets the target frame that this batcher flushes to. /// - public ICanvasFrame TargetFrame => this.targetFrame; + public ICanvasFrame TargetFrame { get; } /// /// Appends one normalized composition command to the pending queue. @@ -61,7 +60,7 @@ public void FlushCompositions() try { CompositionScene scene = new(this.commands); - this.backend.FlushCompositions(this.configuration, this.targetFrame, scene); + this.backend.FlushCompositions(this.configuration, this.TargetFrame, scene); } finally { diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 7438358a8..c3fb6f673 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -7,6 +7,7 @@ using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; using SixLabors.ImageSharp.Drawing.Text; @@ -1177,7 +1178,8 @@ private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true /// private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix) { - List? subPaths = null; + List<(PointF[] Points, RectangleF Bounds, bool IsClosed)>? subPaths = null; + bool allClosed = true; float minX = float.MaxValue, minY = float.MaxValue; float maxX = float.MinValue, maxY = float.MinValue; @@ -1221,7 +1223,8 @@ private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix) RectangleF spBounds = new(spMinX, spMinY, spMaxX - spMinX, spMaxY - spMinY); subPaths ??= []; - subPaths.Add(new PreFlattenedPath(dstPoints, sp.IsClosed, spBounds)); + subPaths.Add((dstPoints, spBounds, sp.IsClosed)); + allClosed &= sp.IsClosed; if (spMinX < minX) { @@ -1249,13 +1252,43 @@ private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix) return Path.Empty; } + RectangleF totalBounds = new(minX, minY, maxX - minX, maxY - minY); + if (allClosed) + { + // Fill path: enforce orientation for NonZero fill rule (ring 0 positive, ring 1+ negative). + if (subPaths.Count == 1) + { + PolygonUtilities.EnsureOrientation(subPaths[0].Points, 1); + return new FlattenedPath(subPaths[0].Points, true, subPaths[0].Bounds); + } + + FlattenedPath[] closed = new FlattenedPath[subPaths.Count]; + for (int i = 0; i < subPaths.Count; i++) + { + PolygonUtilities.EnsureOrientation(subPaths[i].Points, i == 0 ? 1 : -1); + closed[i] = new FlattenedPath(subPaths[i].Points, true, subPaths[i].Bounds); + } + + return new FlattenedCompositePath(closed, totalBounds); + } + + // Stroke centerline: preserve open/closed as-is, no orientation enforcement. if (subPaths.Count == 1) { - return subPaths[0]; + (PointF[] pts, RectangleF bounds, bool isClosed) = subPaths[0]; + return new FlattenedPath(pts, isClosed, bounds); } - RectangleF totalBounds = new(minX, minY, maxX - minX, maxY - minY); - return new PreFlattenedCompositePath([.. subPaths], totalBounds); + // Multiple sub-paths with at least one open — return as a simple wrapper. + // This case is rare (multi-contour stroke centerlines). + FlattenedPath[] parts = new FlattenedPath[subPaths.Count]; + for (int i = 0; i < subPaths.Count; i++) + { + (PointF[] pts, RectangleF bounds, bool isClosed) = subPaths[i]; + parts[i] = new FlattenedPath(pts, isClosed, bounds); + } + + return new FlattenedCompositePath(parts, totalBounds); } /// diff --git a/src/ImageSharp.Drawing/TessellatedMultipolygon.cs b/src/ImageSharp.Drawing/TessellatedMultipolygon.cs deleted file mode 100644 index 5c495ecff..000000000 --- a/src/ImageSharp.Drawing/TessellatedMultipolygon.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Collections; -using SixLabors.ImageSharp.Drawing.Helpers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Compact representation of a multipolygon. -/// Applies some rules which are optimal to implement geometric algorithms: -/// - Outer contour is oriented "Positive" (CCW in world coords, CW on screen) -/// - Holes are oriented "Negative" (CW in world, CCW on screen) -/// - First vertex is always repeated at the end of the span in each ring -/// -internal sealed class TessellatedMultipolygon : IDisposable, IReadOnlyList -{ - private readonly Ring[] rings; - - private TessellatedMultipolygon(Ring[] rings) - { - this.rings = rings; - this.TotalVertexCount = rings.Sum(r => r.VertexCount); - } - - public int TotalVertexCount { get; } - - public int Count => this.rings.Length; - - public Ring this[int index] => this.rings[index]; - - public static TessellatedMultipolygon Create(IPath path, MemoryAllocator memoryAllocator) - { - if (path is IInternalPathOwner ipo) - { - IReadOnlyList internalPaths = ipo.GetRingsAsInternalPath(); - - // If we have only one ring, we can change it's orientation without negative side-effects. - // Since the algorithm works best with positively-oriented polygons, - // we enforce the orientation for best output quality. - bool enforcePositiveOrientationOnFirstRing = internalPaths.Count == 1; - - Ring[] rings = new Ring[internalPaths.Count]; - IMemoryOwner pointBuffer = internalPaths[0].ExtractVertices(memoryAllocator); - RepeatFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, enforcePositiveOrientationOnFirstRing); - rings[0] = new Ring(pointBuffer); - - for (int i = 1; i < internalPaths.Count; i++) - { - pointBuffer = internalPaths[i].ExtractVertices(memoryAllocator); - RepeatFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, false); - rings[i] = new Ring(pointBuffer); - } - - return new TessellatedMultipolygon(rings); - } - else - { - ReadOnlyMemory[] points = [.. path.Flatten().Select(sp => sp.Points)]; - - // If we have only one ring, we can change it's orientation without negative side-effects. - // Since the algorithm works best with positively-oriented polygons, - // we enforce the orientation for best output quality. - bool enforcePositiveOrientationOnFirstRing = points.Length == 1; - - Ring[] rings = new Ring[points.Length]; - rings[0] = MakeRing(points[0], enforcePositiveOrientationOnFirstRing, memoryAllocator); - for (int i = 1; i < points.Length; i++) - { - rings[i] = MakeRing(points[i], false, memoryAllocator); - } - - return new TessellatedMultipolygon(rings); - } - - static Ring MakeRing(ReadOnlyMemory points, bool enforcePositiveOrientation, MemoryAllocator allocator) - { - IMemoryOwner buffer = allocator.Allocate(points.Length + 1); - Span span = buffer.Memory.Span; - points.Span.CopyTo(span); - RepeatFirstVertexAndEnsureOrientation(span, enforcePositiveOrientation); - return new Ring(buffer); - } - - static void RepeatFirstVertexAndEnsureOrientation(Span span, bool enforcePositiveOrientation) - { - // Repeat first vertex for perf: - span[^1] = span[0]; - - if (enforcePositiveOrientation) - { - PolygonUtilities.EnsureOrientation(span, 1); - } - } - } - - public void Dispose() - { - foreach (Ring ring in this.rings) - { - ring.Dispose(); - } - } - - public IEnumerator GetEnumerator() => this.rings.AsEnumerable().GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - internal sealed class Ring : IDisposable - { - private readonly IMemoryOwner buffer; - private Memory memory; - - internal Ring(IMemoryOwner buffer) - { - this.buffer = buffer; - this.memory = buffer.Memory; - } - - public ReadOnlySpan Vertices => this.memory.Span; - - public int VertexCount => this.memory.Length - 1; // Last vertex is repeated - - public void Dispose() - { - this.buffer.Dispose(); - this.memory = default; - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs index 081374ccb..9584a1561 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs @@ -31,19 +31,19 @@ public void LowFontSizeRenderOK(TestImageProvider provider, bool }); c.ProcessWithCanvas(canvas => - { - Brush brush = Brushes.Solid(Color.Black); - Font font = SystemFonts.Get("Tahoma").CreateFont(8); - RichTextOptions options = new(font) - { - WrappingLength = c.GetCurrentSize().Width / 2, - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - Origin = new PointF(c.GetCurrentSize().Width / 2, c.GetCurrentSize().Height / 2) - }; + { + Brush brush = Brushes.Solid(Color.Black); + Font font = SystemFonts.Get("Tahoma").CreateFont(8); + RichTextOptions options = new(font) + { + WrappingLength = c.GetCurrentSize().Width / 2, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + Origin = new PointF(c.GetCurrentSize().Width / 2, c.GetCurrentSize().Height / 2) + }; - canvas.DrawText(options, "Lorem ipsum dolor sit amet", brush, null); - }); + canvas.DrawText(options, "Lorem ipsum dolor sit amet", brush, null); + }); }, testOutputDetails: $"{antialias}", appendSourceFileOrDescription: false); diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs deleted file mode 100644 index 7daea40a1..000000000 --- a/tests/ImageSharp.Drawing.Tests/Rasterization/TessellatedMultipolygonTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Tests.Rasterization; - -public class TessellatedMultipolygonTests -{ - private static MemoryAllocator MemoryAllocator => Configuration.Default.MemoryAllocator; - - private static void VerifyRing(TessellatedMultipolygon.Ring ring, PointF[] originalPoints, bool originalPositive, bool isHole) - { - ReadOnlySpan points = ring.Vertices; - - Assert.Equal(originalPoints.Length + 1, points.Length); - Assert.Equal(points[0], points[points.Length - 1]); - Assert.Equal(originalPoints.Length, ring.VertexCount); - - originalPoints = originalPoints.CloneArray(); - - if ((originalPositive && isHole) || (!originalPositive && !isHole)) - { - originalPoints.AsSpan().Reverse(); - points = points.Slice(1); - } - else - { - points = points.Slice(0, points.Length - 1); - } - - Assert.True(originalPoints.AsSpan().SequenceEqual(points)); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void Create_FromPolygon_Case1(bool reverseOriginal) - { - PointF[] points = PolygonFactory.CreatePointArray((0, 3), (3, 3), (3, 0), (1, 2), (1, 1), (0, 0)); - if (reverseOriginal) - { - points.AsSpan().Reverse(); - } - - Polygon polygon = new(points); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, reverseOriginal, false); - Assert.Equal(6, multipolygon.TotalVertexCount); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void Create_FromPolygon_Case2(bool reverseOriginal) - { - PointF[] points = PolygonFactory.CreatePointArray((0, 0), (2, 0), (3, 1), (3, 0), (6, 0), (6, 2), (5, 2), (5, 1), (4, 1), (4, 2), (2, 2), (1, 1), (0, 2)); - if (reverseOriginal) - { - points.AsSpan().Reverse(); - } - - Polygon polygon = new(points); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - - VerifyRing(multipolygon[0], points, !reverseOriginal, false); - } - - [Fact] - public void Create_FromRecangle() - { - RectangularPolygon polygon = new(10, 20, 100, 50); - - PointF[] points = polygon.Flatten().Single().Points.Span.ToArray(); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, true, false); - Assert.Equal(4, multipolygon.TotalVertexCount); - } - - [Fact] - public void Create_FromStar() - { - Star polygon = new(100, 100, 5, 30, 60); - PointF[] points = polygon.Flatten().Single().Points.Span.ToArray(); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, true, false); - } -} diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png index 08cc87986..617a48cc1 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc680ab6ee1d5d28f6b35e959402d0ee28f314a9cfc5df39cb04f234e9b51b8f -size 11082 +oid sha256:45bbd1bf6fc5881cedf0036bca2e122c4a9add1e3e6183c474d1f6889e2d791c +size 11014 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png index 829180a52..de894f52b 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ab6a73fbda08c85580c7f326fb53ac3e51aa16ff6c16d95813e4feee29b5fb0 -size 11082 +oid sha256:937de251b66cbb20c88d46b6e105c60725fb448d7fd9ad106c0e6f3439cb07ec +size 11014 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png index a7585cd7a..c3e55d441 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2195ff2337653059cd3ff9d13ffc7301e23f7e5adc7a618a2404b0a1fb247ffb -size 5338 +oid sha256:4e4c861a6b42517bc868f41dbd307af168e8a6cc4145e501b4c64556c0a26430 +size 5334 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 3c5316a46..59f0e63e3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0c3b48846b5b64a99de9cf1c76bfaa6bfa9f6644803e0139543c2d4e82c58fa -size 4377 +oid sha256:4ac4bdbbbc73fa14cc311ae2af518b23faf0c7a1226dda9f60d237ecdd7ab2ae +size 4384 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png index 9eec6b510..c448c1eb5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c48c78a126137422b0ecc63dabee18c728b8c64737eeae39127225c96fb497a6 -size 2603 +oid sha256:bde8bcf94782a81d9575b4c0bca1c20100080db2b57db41ff226c386e1a7bc24 +size 2599 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png index 85bc516d9..262b726c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd -size 2648 +oid sha256:80b6836e8d935831e9fdb2b387c9b395c6c6ebcf45aaeaee5fcda788fd13a241 +size 2639 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png index 85bc516d9..262b726c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd -size 2648 +oid sha256:80b6836e8d935831e9fdb2b387c9b395c6c6ebcf45aaeaee5fcda788fd13a241 +size 2639

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 diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png deleted file mode 100644 index 0472c7b967e2105340d4b2079ee26a4b7d342297..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368423 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%0D)Fd30=LBMPyZbxP)zbxAUhdxRZi32` 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%0D}?y&`YAfm!}gsu#QFW&nZEKfdM+&EWs@ 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