From 11eabed4ef1009ef40319480270255337f3c4bdb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 20 Feb 2026 12:38:19 +1000 Subject: [PATCH 01/86] 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 74e8e154..c7e333c0 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 00000000..520dd93b --- /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 00000000..14b43d01 --- /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 00000000..73379cae --- /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 00000000..f158b35c --- /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 00000000..b0fc6f67 --- /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 153c102b..338b613d 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 00000000..f8ee17f3 --- /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 00000000..c5c3315a --- /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 cd2c22ed..00000000 --- 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 00000000..0fdaaae7 --- /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 00000000..bff81244 --- /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 92f6f56e..5e9b5aae 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 6b13530f..6ae966e4 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 99b16023..54c7b6a5 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 00000000..1a9d74fa --- /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 fbe4233f..746f250a 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 065125c6..9aafa908 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 5a8062e3..cc4fb6ff 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 22692dc0..a4445032 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 ef315427..e5a41196 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 92bf3db8..ff5a328d 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 5b3a5cc8..3efbf161 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 5dfadb97..8b780fe9 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 0788cb6f..a1ea7aaa 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 9e1e3c86..0fe36075 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 a8f60b24..a663a7c1 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 1914f4bb..ef2d657b 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 6436d67e..ec3b59b3 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 6e13391b..1fcae55f 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 db2361cf..510101e1 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 c9369761..1592c644 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 268592a6..383a0677 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 41c3e071..9d3a0407 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 5aed6678..2ef27c2f 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 150b2612..480d7a63 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 e4fb2445..8ca95585 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 a7b7f056..c082ef74 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 00000000..442658d1 --- /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 00000000..dc86d0b2 --- /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 00000000..6910f758 --- /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 0d49fa0e..f2d71700 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 ffea506c..3effa1a4 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 02/86] 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 14b43d01..fae29af2 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 f158b35c..d9c050b8 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 b0fc6f67..59256719 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 00000000..1d1efce0 --- /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 0fdaaae7..779e23ce 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 1a9d74fa..ad464d60 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 c3080014..32a0c22e 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 0a2f32ce..f85acbe3 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 03/86] 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 73379cae..4a7dd541 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 d9c050b8..4377879f 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 59256719..01298109 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 779e23ce..c3c58e22 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 32a0c22e..470377a6 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 6910f758..82ba7567 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 04/86] 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 00000000..efc379b8 --- /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 01298109..c68cd937 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 05/86] 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 c68cd937..583eb461 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 06/86] 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 583eb461..7e9b2963 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 00000000..788ea67c --- /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 00000000..2714017f --- /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 00000000..29eefb19 --- /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 c3c58e22..3456b956 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 00000000..f3cb2e79 --- /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 5e9b5aae..53f3b34e 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 00000000..5791ae03 --- /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 ad464d60..19f9c51e 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 a1ea7aaa..62541f74 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 442658d1..d6f93d0d 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 3effa1a4..d206e690 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 07/86] 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 520dd93b..d1af7ad2 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 7e9b2963..d495ff05 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 00000000..4a9c609d --- /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 1d1efce0..7a4ed931 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 788ea67c..fe821cbd 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 00000000..26887b18 --- /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 3456b956..811f85cb 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 53f3b34e..4c06e72e 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 00000000..d1818cdf --- /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 19f9c51e..3be9bbe9 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 470377a6..82e89c16 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 d6f93d0d..2ba22040 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 82ba7567..e9cfc15e 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 d206e690..5b53a031 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 08/86] 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 4a7dd541..af450357 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 00000000..c80e737a --- /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 d495ff05..7c24c5f8 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 00000000..7db484b2 --- /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 26887b18..1024ceb1 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 00000000..3bb60974 --- /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 811f85cb..42d5502f 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 4c06e72e..d23d3ca6 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 00000000..44ad6b17 --- /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 d1818cdf..e9ba7485 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 3be9bbe9..019f03bf 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 ef2d657b..4ea3bbf0 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 ec3b59b3..25a60264 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 00000000..3981cb8c --- /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 2ba22040..c59c690f 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 00000000..0ee8348e --- /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 5b53a031..8124edfa 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 09/86] 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 efc379b8..9f4c3d8c 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 c80e737a..00000000 --- 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 7c24c5f8..cd390cb7 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 00000000..8778c90c --- /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 00000000..130875fc --- /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 4a9c609d..19a4d248 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 7a4ed931..55253504 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 fe821cbd..43729ff4 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 00000000..2bc92c66 --- /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 00000000..278f4690 --- /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 00000000..07def310 --- /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 7db484b2..217ad9a4 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 1024ceb1..def34596 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 42d5502f..ee4cdc78 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 e9ba7485..0d880c80 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 25a60264..45b403fc 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 9d3a0407..dc944d68 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 3981cb8c..4b6ee94b 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 c082ef74..b127c714 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 e9cfc15e..10a4764b 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 00000000..324321d7 --- /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 10/86] 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 cd390cb7..c8821db3 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 8778c90c..231a5a82 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 ee4cdc78..2ffc3ef2 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 0d880c80..2567b4f6 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 019f03bf..e2e05ad2 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 10a4764b..dde036b6 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 11/86] 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 00000000..faac7974 --- /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 c8821db3..2dae5c21 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 231a5a82..c70e81f0 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 e2e05ad2..74d54c1f 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 12/86] 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 2dae5c21..913d78c6 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 c70e81f0..1380a360 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 2ffc3ef2..d09727b0 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 13/86] 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 913d78c6..3976993b 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 d09727b0..c5e7038e 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 d23d3ca6..a65c0725 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 2567b4f6..17ee96fd 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 74d54c1f..f3cefc46 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 4b6ee94b..a4ceb951 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 c59c690f..7ebf0719 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 0ee8348e..8cd75c8d 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 8124edfa..dd82f4c2 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 14/86] 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 f3cefc46..d0c9d625 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 15/86] 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 def34596..89e2085e 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 d0c9d625..1b3c811e 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 a4ceb951..b1e181be 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 16/86] 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 45b403fc..d7e0a187 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 2966e3f7..846d7e79 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 5f317f2e..378591ee 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 706b772a..2c843e2e 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 17/86] 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 00000000..51c09f56 --- /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 00000000..aedc93b3 --- /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 d1af7ad2..00000000 --- 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 00000000..b602ecc3 --- /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 00000000..bb1a651a --- /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 00000000..aeb09d7d --- /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 af450357..332ad894 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 00000000..de5b34ed --- /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 faac7974..e8d410f2 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 3976993b..651dc12a 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 1380a360..15aa8850 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 19a4d248..5c496c12 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 c5e7038e..cb1a2948 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 a65c0725..c5f319d3 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 17ee96fd..b47b7c7f 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 1b3c811e..245a9d7b 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 cc4fb6ff..6198c2aa 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 7ebf0719..3fc4441a 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 dde036b6..1de55e78 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 8cd75c8d..1acdee74 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 00000000..6a3b6f09 --- /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 dd82f4c2..e1909c27 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 18/86] 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 51c09f56..dcd17a79 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 00000000..5726aa5a --- /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 b602ecc3..1c2484fe 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 bb1a651a..9cccf2f0 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 15aa8850..168ac909 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 19/86] 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 278f4690..33882361 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 20/86] 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 dcd17a79..7907810a 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 00000000..5c615a74 --- /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 aedc93b3..aa05904e 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 5726aa5a..7d401318 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 1c2484fe..57ebefda 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 00000000..d68d7394 --- /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 00000000..5518d502 --- /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 3ade0cc6..11587f06 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 168ac909..e2344d8a 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 130875fc..2b9878c0 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 43729ff4..4c8f78d6 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 61f5bb21..b0589520 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 1de55e78..0e31370f 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 21/86] 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 7907810a..00000000 --- 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 5c615a74..00000000 --- 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 aa05904e..00000000 --- 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 7d401318..00000000 --- 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 57ebefda..00000000 --- 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 c760bafb..00000000 --- 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 5518d502..00000000 --- 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 d68d7394..e694e789 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 00000000..4d5fcfdb --- /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 11587f06..9b0cfabc 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 e2344d8a..a5f9e67b 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 299ac333..c6e02256 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 b47b7c7f..444ee17f 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 0e31370f..75e526b7 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 22/86] 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 4d5fcfdb..8e25641b 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 9b0cfabc..62e66660 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 00000000..2393ce20 --- /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 00000000..e337d54c --- /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 cb1a2948..379d0568 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 c5f319d3..5ccec307 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 444ee17f..e34bc73f 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 3fc4441a..0ccca340 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 1acdee74..51001ed7 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 e1909c27..c6e83feb 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 23/86] 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 00000000..4dcf0dd9 --- /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 9f4c3d8c..ad01234a 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 8e25641b..02f712a7 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 62e66660..311f1bd2 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 a5f9e67b..dffee547 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 379d0568..c0014675 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 e34bc73f..ee6743c5 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 24/86] 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 311f1bd2..e212cca8 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 dffee547..8f240c5d 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 217ad9a4..d05a451b 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 75e526b7..9c2fe235 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 25/86] 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 00000000..4ae00664 --- /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 00000000..da21ec2b --- /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 4dcf0dd9..b8e96fb0 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 00000000..ccd2ae77 --- /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 fdadefbf..00000000 --- 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 e212cca8..2c0e3653 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 8f240c5d..7016ea06 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 5c496c12..00000000 --- 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 b0589520..bf767170 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 89e2085e..67d4520d 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 b1e181be..d0a13474 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 9c2fe235..db81d043 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 26/86] 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 da21ec2b..d6f49451 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 ccd2ae77..336c36de 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 2c0e3653..d4b4b46f 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 db81d043..62d60e06 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 27/86] 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 d6f49451..6c0de670 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 00000000..b0e9fc38 --- /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 00000000..4e3734cd --- /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 00000000..db7e2a39 --- /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 d4b4b46f..c7b00e7b 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 28/86] 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 6c0de670..00000000 --- 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 00000000..2d6d1240 --- /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 b0e9fc38..07b77186 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 4e3734cd..a196ddd7 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 db7e2a39..ec748f5b 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 00000000..2dd8c74c --- /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 b8e96fb0..b4e59def 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 336c36de..5141c7e7 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 c7b00e7b..013d1caf 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 7016ea06..7561ef71 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 bf767170..0c14037d 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 29/86] 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 00000000..010c023f --- /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 2d6d1240..8052bd6a 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 07b77186..2062dfa9 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 00000000..d8f5dcd6 --- /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 a196ddd7..e554b167 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 ec748f5b..00000000 --- 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 2dd8c74c..00000000 --- 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 b4e59def..5c242fd8 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 5141c7e7..b85a6a10 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 013d1caf..79aa646d 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 d0a13474..3f1ed193 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 30/86] 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 4ae00664..fd1043d9 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 84e0759c..ad717240 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 010c023f..391552b8 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 8052bd6a..9744e14e 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 2062dfa9..e90b0299 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 d8f5dcd6..784a1736 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 e554b167..d3acb77e 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 5c242fd8..1e5a925d 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 de5b34ed..1d5ee20c 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 ad01234a..74724186 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 b85a6a10..cfbe15a4 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 79aa646d..ece0df38 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 7561ef71..58f8102a 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 0c14037d..69d46777 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 07def310..21d79f48 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 31/86] 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 c0014675..625e381a 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 245a9d7b..3aefd9ca 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 f60e0f21..6813c8fc 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 f5ea9a1f..36505d90 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 00000000..900e4b29 --- /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 00000000..487076ba --- /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 3efbf161..f4335778 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 00000000..6549a325 --- /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 8b780fe9..01065c76 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 62541f74..e6b337a2 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 0fe36075..4e74e002 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 3f1ed193..d295d072 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 3fc6f89f..5739567e 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 62d60e06..cf9ae754 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 6a3b6f09..57150ab8 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 32/86] 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 338b613d..e65da8e0 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 33/86] 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 3aefd9ca..cf2b2a4d 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 487076ba..a1237223 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 e6b337a2..2642a1d9 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 34/86] 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 00000000..c6a4e5f3 --- /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 cf2b2a4d..e58f5d66 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 a1237223..c942b2fe 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 6549a325..ec22ed8f 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 2642a1d9..d44a46fc 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 4e74e002..5bfe77c8 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 a663a7c1..23dce964 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 994753da..b897d1d9 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 d295d072..219c9eb9 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 cf9ae754..396fac3c 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 51001ed7..f092b14d 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 57150ab8..117618bb 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 35/86] 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 e65da8e0..f9686283 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 625e381a..fb6ccfa6 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 36/86] 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 c6a4e5f3..1b3e497c 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 e58f5d66..9d3b75a8 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 37/86] 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 9d3b75a8..a1c58363 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 38/86] 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 ece0df38..75f5e96f 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 58f8102a..2b1bc320 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 f9686283..89139a46 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 1b3e497c..24a3821b 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 a1c58363..9b9c340e 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 d22aff79..00000000 --- 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 39/86] 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 9b9c340e..704d47a4 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 00000000..dab8a1f4 --- /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 00000000..a86ade0d --- /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 00000000..cb0d76dc --- /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 00000000..689806c8 --- /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 117618bb..00000000 --- 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 00000000..59bf3d9e --- /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 00000000..7cdab4b1 --- /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 00000000..07f4e00e --- /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 00000000..bf557661 --- /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 00000000..2fbbdb86 --- /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 00000000..84eb2fd0 --- /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 00000000..5d684ce8 --- /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 00000000..e454e4a7 --- /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 00000000..d4dde3da --- /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 00000000..c79ff558 --- /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 00000000..20072c18 --- /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 00000000..95a15385 --- /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 00000000..d134819a --- /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 00000000..1f838f13 --- /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 00000000..7324dfbe --- /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 00000000..46017f65 --- /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 00000000..d68fe1bd --- /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 00000000..24acb55d --- /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 00000000..874e0d50 --- /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 00000000..41e7ad63 --- /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 00000000..84033c93 --- /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 00000000..d7764d16 --- /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 00000000..a9af2482 --- /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 00000000..cf690043 --- /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 00000000..f747c34a --- /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 00000000..1af1d92d --- /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 00000000..47e1947b --- /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 00000000..353dd327 --- /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 00000000..7c504510 --- /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 00000000..0f94e996 --- /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 00000000..9c1c6a7a --- /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 00000000..312bf9c4 --- /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 00000000..ec1d63e7 --- /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 00000000..b46e34bc --- /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 00000000..189e5a0e --- /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 40/86] 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 704d47a4..8963050c 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 dab8a1f4..ddfc43ba 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 00000000..5d2f4c96 --- /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 a86ade0d..8e41345a 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 cb0d76dc..938bd672 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 689806c8..e7d017c8 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 1f838f13..2d562e01 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 41/86] 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 8963050c..af23b64a 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 5d2f4c96..770400de 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 846d7e79..76213345 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 95a15385..a6d20c33 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 00000000..d6518878 --- /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 00000000..b13d694d --- /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 42/86] 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 a6d20c33..f4101055 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 43/86] 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 cfbe15a4..106c7035 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 00000000..564cba7a --- /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 fb6ccfa6..60d7a259 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 5ccec307..43865750 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 ee6743c5..c3302113 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 af23b64a..55678d28 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 770400de..8f85739e 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 0ccca340..67cdb637 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 396fac3c..d944ca61 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 f092b14d..81c36548 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 00000000..a8bc7d8d --- /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 c6e83feb..88c9f54e 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 00000000..48a94aa6 --- /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 00000000..6a980231 --- /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 00000000..6a980231 --- /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 00000000..6a980231 --- /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 44/86] 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 cc2fc17f..00000000 --- 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 00000000..6d4c9dcc --- /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 00000000..87de3c0a --- /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 45/86] 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 55059018..00000000 --- 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 6d4c9dcc..95d55b8c 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 00000000..7838042d --- /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 46/86] 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 b2ba8752..00000000 --- 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 7838042d..6041efac 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 47/86] 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 34026ab5..00000000 --- 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 6041efac..a42b8253 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 0e1070e7..00000000 --- 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 0b71e15a..00000000 --- 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 0e639df5..00000000 --- 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 9a7f7901..00000000 --- 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 00000000..eaff6def --- /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 00000000..2aadc6ef --- /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 00000000..dade3449 --- /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 00000000..84836faa --- /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 48/86] 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 b01b83c1..00000000 --- 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 425bdba8..00000000 --- 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 a42b8253..2d4df04c 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 3d94259f..00000000 --- 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 2bd89ce8..00000000 --- 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 c5206d91..00000000 --- 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 c667647e..00000000 --- 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 130ae703..00000000 --- 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 ff7d8b68..00000000 --- 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 141ca949..00000000 --- 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 2bbf451e..00000000 --- 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 609fc357..00000000 --- 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 fb196598..00000000 --- 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 87e3affc..00000000 --- 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 6f8346d1..00000000 --- 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 00000000..1e09fdf4 --- /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 00000000..17f5c20a --- /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 00000000..886c5059 --- /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 00000000..2789f97c --- /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 00000000..1f446a1a --- /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 00000000..8ad912ce --- /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 00000000..1c9bc57a --- /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 00000000..09abafc7 --- /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 00000000..0705678b --- /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 00000000..f2128189 --- /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 00000000..5b98c71a --- /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 00000000..1f5ff275 --- /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 49/86] 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 81d77075..00000000 --- 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 2d4df04c..7dc53d5c 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 50/86] 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 0bedd4d3..00000000 --- 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 00000000..4588ca54 --- /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 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 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 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yv7|ftIx;Y9?C1WI$O`0h7I;J! zGcfQS24TkI`72U@YCd|pIEGZ*dVBZxmo->1WGn^7Eg)y?K7#w;FXX&BIK8 z7IV5YFfleZI5;RM2ncX+u&^*OF@i85A&5#8RTx4r^N>_w2*GUqzjn3E!Ts45PkC4l z9bMbukypmU^iV0><4nn)i!*<5Syn zt_me~{Xe%x-#FbYcrxp_lE5w9BCEK4oj;Y+e9ugqaeQvK!wZ?0x^K2WIkfNeMvj)U z?lZ?uf1FXgb3=X>_wRyZH#u6KJ)RN#vy?Zv`b-=|ze!x*(?fN+dT;oT>OOMc&!ku@ z@EOQ6FX7%jKZ~=as{2a*X1m^>nrV?|)*4zT&nq-zT3D(7?8v0nzY}-3mRU(P2C&J0 z75nxp?6>dE4fapp12sK+J3~LI?sRLdGtjBUeQpX<4lb3916K zt`{fT?@dXpzjl$M#mM4P^o{e!W8{m|_1;v^>37h0IyL6T^D6G&eK9wtTgo-|{MP-H zx<}@P?weq-vPOefOK1FAeE)rt-jO>J!SyR&o!sGi4WcUOK7Xb1vkbjA*N?2(yn(yr z9Jk^7<7d8!eLHJdz2~r=z^Ml{dT-i`6W7;EW$$j4ZR|*i&wRh(`q95WdT*qY%Z}?l z`tHW!RPj@Y`}dN^f+u#|S7Lws^lZ$HdOhy>Cw6Sua6dm$oa64HI-qCnCx<;f^e^L% z(&avPg~xT>XX26)>u>$!XbGCvU%7lszusEz-=&7zkH6`PPFV~w>+{~$zrQcp?b@RE zCi?cFJ_nbe>0h@e*}c|%^VL$B-$g$f==B$3-_|ET0|u1#GedV}fl~|KDXc$MBlOw% z%+;l~*-!56aNTAt-)OLF!~D0pZ+6S}I%qU=@7y5&O83p>+(h|z1$Ime_a2TD|MqLm z#G~!a&w#e9h>O{@VSOGj1*~2y$dMXgzwXP{OY=;F#J}A;>?p`#I_WP^|M}Kh*T@Qs z7X_O)3v!&@7jwh^+5YDs&D%YAlmAa@t=-FCvEz=K%)$Q}lb8iv{z~tgymO7dolMEK zn>!pc#K2~?ua@a{$k^`jcjdL+ORFt1VsDsRir-JV=vrhUe}CKBE&K=OW#(_;m+6&m z2@(I6ymN!TT{cVO1fYi9QGeT;*A=QUC7Mjy8FSlhBN{K0JdScZkE3|{JDRYG3)=>SWCVrmBY^kPl*;Q3G`LhC`~%= z)2S=wzS<*a^)l&?^RFlG5q!M$N0nw=_@f(rKLt4sKbhX6@<>AOctlq6uUhLi2bWFL zw2ue56=@z@EqPM;v$DXeZL8RdG`Giy-=1-7#+n&&jbHY4z1bqVc#Vu{-)ZBCF&&S; zpPA?|r+I1g+b5zw>*}Z1u)3GAF%m zhfUSUl6iV3#qjt~i!h~|CjLw>EhkmyMu#d#p7BuXdl(~qIsdV(^fONdTlXUK?V&r1 zuG}xUJEQZ-?27ZdDuq1GzdN%=#@uLia*r?#zf3w*R=#>P4GI#X`O za4xy-am{$rov5o8UMkl=FE7yQJ3hn4B%JA`>7=baD&f6S$0bg#>`BqP_x%7^aoFv$ zRcuD{3#V{*$@9N`oITk>F1hGFM9OGVjA@pP>ZJZC1=C56Dj*L&Ejt&;JoB{0F^?{d z6sxUwf&Q3ilk;5W@v6Yb8rzrG++bQcQO2TB93-6aT*h>Q;$sPAP5#y!{f~dwvFc2i zr@ZsNSP#gDI?rC$xovu$w8){VKZWOs!efo^g|z~UZDzcSbbGW-q_Jf0T`P+yp#8}z z=Zz-Cnp=oj zkuy6}%pYq^+#~+@wZzk~U$^b{X@i7wfiAta>e$8trAg6pEQ%nZ?B_D6(V>eq3g^u2 zTw;^?ZyP9XCa&2&b|=Uq zJLznU=eAp?3MEc%?Rlafew+1DwaV?ca~9{U_7XAe6FPZSBL2wt|6f8X8!yb;{Z&PI zXO+?<`#-gl#Ldn--%~$+Iw53Z>3p(X!rnq&kmImdKI;Vst00f^YRsaA1 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 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/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 00000000..5c9d0cae --- /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 00000000..c5042fd7 --- /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 00000000..5cac37f4 --- /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 00000000..ae5f235f --- /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 00000000..1ae34ed2 --- /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 00000000..0c599000 --- /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 00000000..2f38a950 --- /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 00000000..5c9d0cae --- /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 00000000..aad3872b --- /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 00000000..8a06ad3b --- /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 00000000..1abf93c6 --- /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 00000000..c5042fd7 --- /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 00000000..2ca11b59 --- /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 00000000..4470c579 --- /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 00000000..09f47d53 --- /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 00000000..5cac37f4 --- /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 00000000..e0b5752c --- /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 00000000..d3c717eb --- /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 00000000..71ac1375 --- /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 00000000..ae5f235f --- /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 00000000..15a94655 --- /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 00000000..97603b56 --- /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 00000000..ae5f235f --- /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 00000000..1fd9d970 --- /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 51/86] 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 7afd37a4..d6b348d2 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 00000000..7f87f9d4 --- /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 00000000..7f87f9d4 --- /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 52/86] 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 9b823400..00000000 --- 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 4588ca54..9d04f5db 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 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 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 00000000..1909adfc --- /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 00000000..ed9c7271 --- /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 00000000..6fb508f1 --- /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 00000000..7eb5bc6d --- /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 00000000..ac230448 --- /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 00000000..f9a9c52a --- /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 00000000..8fb3dcbc --- /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 00000000..b56336ea --- /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 00000000..95c37857 --- /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 00000000..ee0dd570 --- /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 00000000..ee0dd570 --- /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 00000000..ee0dd570 --- /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 00000000..12464c6f --- /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 00000000..f2d7da91 --- /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 00000000..dddebf3d --- /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 00000000..34978f1e --- /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 00000000..c512f35c --- /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 00000000..bb599f23 --- /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 00000000..5f7077b4 --- /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 00000000..c1be2838 --- /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 00000000..1348e785 --- /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 00000000..c024c490 --- /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 00000000..1fd9d970 --- /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 53/86] 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 bdbd43e0..00000000 --- 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 00000000..4a0911c2 --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 00000000..f93de56a --- /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 00000000..28c0a0bc --- /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 00000000..28c0a0bc --- /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 54/86] 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 e5a41196..3e47f6c7 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 15750e4c..f51dee4a 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 145fba14..00000000 --- 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 00000000..4d9445bc --- /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 00000000..2588ada2 --- /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 00000000..2588ada2 --- /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 00000000..738f02c6 --- /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 00000000..48b5a132 --- /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 00000000..48b5a132 --- /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 00000000..738f02c6 --- /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 00000000..c86a8c7f --- /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 00000000..c86a8c7f --- /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 00000000..bfa7f3be --- /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 00000000..bfa7f3be --- /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 00000000..b6abcd26 --- /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 00000000..b6abcd26 --- /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 55/86] 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 1a6bcb5e..00000000 --- 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 00000000..2d4375a0 --- /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 56/86] 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 c7e333c0..318aeae9 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 89139a46..2eee14a2 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 e7379712..e06c7b18 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 00000000..420694cd --- /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 00000000..f664fcd7 --- /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 00000000..c12ab560 --- /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 00000000..3de5762f --- /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 00000000..7ead6c60 --- /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 00000000..e072cda8 --- /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 00000000..6424b742 --- /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 00000000..30e9f56d --- /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 ac230448..382323e5 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 00000000..b34e8deb --- /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 00000000..8953a1a8 --- /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 00000000..70d555da --- /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 00000000..a3c036f7 --- /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 00000000..19242c79 --- /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 00000000..2c8dd12b --- /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 1365aa90..00000000 --- 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 483091b7..00000000 --- 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 95806e72..00000000 --- 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 cb39952c..00000000 --- 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 ebebcb87..00000000 --- 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 a9d95b2b..00000000 --- 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 12024df6..00000000 --- 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 9b8104f7..00000000 --- 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 8dad5340..00000000 --- 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 37e3bd5f..00000000 --- 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 0aa68114..00000000 --- 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 864ffbf1..00000000 --- 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 12ac94d0..00000000 --- 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 d839ae8e..00000000 --- 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 57/86] 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 3fbba995..00000000 --- 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 00000000..43f43f17 --- /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 00000000..bb6ada2b --- /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 00000000..a372ec97 --- /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 00000000..68dea434 --- /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 00000000..38361a37 --- /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 00000000..d7037317 --- /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 00000000..e9c92bb5 --- /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 00000000..50bca02e --- /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 00000000..501ddf10 --- /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 00000000..f334102c --- /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 00000000..9823b0c1 --- /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 00000000..428df318 --- /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 00000000..7274161b --- /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 00000000..d86b8959 --- /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 00000000..197e0a11 --- /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 58/86] 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 5739567e..834ad6f6 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 36bc63aa..00000000 --- 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 d029a873..00000000 --- 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 d029a873..00000000 --- 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 7a434207..00000000 --- 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 7a434207..00000000 --- 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 15431f30..00000000 --- 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 4e29cc25..00000000 --- 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 3fe215ed..00000000 --- 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 8ad422f6..00000000 --- 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 c7cb0018..00000000 --- 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 00000000..666fead7 --- /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 00000000..666fead7 --- /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 00000000..666fead7 --- /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 00000000..666fead7 --- /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 00000000..0f374410 --- /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 00000000..0f374410 --- /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 00000000..9f0037f8 --- /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 00000000..9f0037f8 --- /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 00000000..9f0037f8 --- /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 00000000..9f0037f8 --- /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 00000000..c878a2d6 --- /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 00000000..c7c963e0 --- /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 00000000..fa919bbd --- /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 00000000..5a722356 --- /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 00000000..e191ef63 --- /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 00000000..4c06a02b --- /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 00000000..8ab2e075 --- /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 00000000..81a10570 --- /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 00000000..ab622192 --- /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 00000000..ab622192 --- /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 00000000..ce8a6b4e --- /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 00000000..b54d96c7 --- /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 00000000..61319dd4 --- /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 00000000..722981db --- /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 00000000..5080ee1f --- /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 00000000..fbb6127c --- /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 00000000..66151f9b --- /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 00000000..66151f9b --- /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 00000000..a1276627 --- /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 00000000..a4287c10 --- /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 00000000..46efe9db --- /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 00000000..d39599e3 --- /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 00000000..c10fe137 --- /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 00000000..83cca969 --- /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 00000000..d78c4a20 --- /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 00000000..2136151b --- /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 00000000..5420e25e --- /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 00000000..36e469dc --- /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 59/86] 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 3fb5cbf9..00000000 --- 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 9d04f5db..da749723 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 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 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 00000000..c2c16420 --- /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 00000000..88b563c8 --- /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 00000000..8b11acdd --- /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 00000000..ae98e58e --- /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 00000000..2cf1f58b --- /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 00000000..3f00c233 --- /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 60/86] 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 8ecbdef4..00000000 --- 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 00000000..77f44a16 --- /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 61/86] 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 cc4518e6..00000000 --- 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 da749723..7fbdc91f 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 718c00be..00000000 --- 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 41a7c333..00000000 --- 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 4de5fe10..00000000 --- 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 6edeb18d..00000000 --- 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 00000000..0449c3a7 --- /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 00000000..5c94cdb6 --- /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 00000000..abde514b --- /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 00000000..384c6ec1 --- /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 62/86] 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 289971c8..328154e1 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 00000000..cebcbc75 --- /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 00000000..d108058d --- /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 00000000..49875359 --- /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 00000000..612d67db --- /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 00000000..4bde3c32 --- /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 00000000..19c8c211 --- /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 63/86] 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 11e80c5c..00000000 --- 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 7dc53d5c..90b65c12 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 00000000..4406ac4f --- /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 00000000..9bc8ba0f --- /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 2ec013d5..00000000 --- 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 266a6d6b..00000000 --- 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 64/86] 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 f6464394..00000000 --- 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 00000000..50ea36b3 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..7bb7d286 --- /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 00000000..6b10adb5 --- /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 00000000..ee46aaf5 --- /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 00000000..761b4c89 --- /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 00000000..6b10adb5 --- /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 00000000..ee46aaf5 --- /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 00000000..6b10adb5 --- /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 00000000..ee46aaf5 --- /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 00000000..6b10adb5 --- /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 00000000..ee46aaf5 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..b794bca2 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..8c0d1942 --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..f7603337 --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..f3ec6a94 --- /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 00000000..f3ec6a94 --- /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 00000000..bb11e621 --- /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 00000000..f3ec6a94 --- /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 00000000..f3ec6a94 --- /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 00000000..10a1ae29 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e116823e --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..e6eab74a --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..63d19c05 --- /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 00000000..0848b759 --- /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 00000000..63d19c05 --- /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 00000000..63d19c05 --- /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 00000000..f7603337 --- /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 00000000..0848b759 --- /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 00000000..0848b759 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..ee46aaf5 --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..9dea11fe --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..da321f0c --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..25b9ebb5 --- /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 00000000..b878a6e4 --- /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 00000000..0c47c88f --- /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 00000000..c255b918 --- /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 00000000..cd1c71dd --- /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 00000000..5537e66d --- /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 00000000..482e8605 --- /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 00000000..797a55a9 --- /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 00000000..9f3e8dd3 --- /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 00000000..824249ec --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..97f49f60 --- /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 00000000..5bd70632 --- /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 00000000..724671ee --- /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 00000000..99589a93 --- /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 00000000..62033d73 --- /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 00000000..091fc37a --- /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 00000000..d737910a --- /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 00000000..a7341716 --- /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 00000000..c908b812 --- /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 00000000..084ff1bc --- /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 00000000..b878a6e4 --- /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 00000000..0c47c88f --- /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 00000000..797a55a9 --- /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 00000000..cd1c71dd --- /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 00000000..5537e66d --- /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 00000000..dd9dbe56 --- /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 00000000..c255b918 --- /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 00000000..9f3e8dd3 --- /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 00000000..a2661c8a --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..b0ecbb4d --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..22d5914f --- /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 00000000..02c03224 --- /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 00000000..aaaa001c --- /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 00000000..8f661c05 --- /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 00000000..09881b5e --- /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 00000000..02c03224 --- /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 00000000..111778ee --- /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 00000000..2a045317 --- /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 00000000..cc67c91c --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..f30bc174 --- /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 00000000..bb11e621 --- /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 00000000..1c6c67d9 --- /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 00000000..0ee00c74 --- /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 00000000..cb9431bb --- /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 00000000..0ee00c74 --- /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 00000000..e94068a0 --- /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 00000000..aa07708e --- /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 00000000..1b1953c5 --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..442db9eb --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..22d5914f --- /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 00000000..02c03224 --- /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 00000000..111778ee --- /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 00000000..8f661c05 --- /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 00000000..09881b5e --- /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 00000000..8f661c05 --- /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 00000000..aaaa001c --- /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 00000000..2a045317 --- /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 00000000..b524d9dc --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..b8535b73 --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..22d5914f --- /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 00000000..02c03224 --- /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 00000000..aaaa001c --- /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 00000000..8f661c05 --- /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 00000000..09881b5e --- /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 00000000..02c03224 --- /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 00000000..111778ee --- /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 00000000..2a045317 --- /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 00000000..cc67c91c --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..cbfd0127 --- /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 00000000..22d5914f --- /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 00000000..02c03224 --- /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 00000000..aaaa001c --- /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 00000000..8f661c05 --- /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 00000000..09881b5e --- /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 00000000..02c03224 --- /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 00000000..111778ee --- /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 00000000..2a045317 --- /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 00000000..cc67c91c --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..bb11e621 --- /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 00000000..f30bc174 --- /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 00000000..bb11e621 --- /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 00000000..1c6c67d9 --- /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 00000000..0ee00c74 --- /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 00000000..cb9431bb --- /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 00000000..0ee00c74 --- /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 00000000..e94068a0 --- /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 00000000..aa07708e --- /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 00000000..1b1953c5 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..0ee00c74 --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..22d5914f --- /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 00000000..02c03224 --- /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 00000000..111778ee --- /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 00000000..8f661c05 --- /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 00000000..09881b5e --- /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 00000000..8f661c05 --- /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 00000000..aaaa001c --- /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 00000000..2a045317 --- /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 00000000..b524d9dc --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..8f661c05 --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 00000000..50c109be --- /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 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 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 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!Z!nov&{$B>F!Z|}x>izG@kJlvT7yP?BRNlBw)(d2(I`xnWJaa?tD znIt+RN^0iR%)0#dll!+g&YHdZ-I+I~2a5$b@;;y2`}y5Foq4Mot_fRl$O>2~lsLR- zxWKrO*@?Y{4}_5gn_&_#L4}fb22>?LeGt7MB?cWArrA@1{}iw91JG@2}tE?Z4h{%k%x4Lee(1PAu>KE-9D( z!5}F7%Z+>Io|M&!9Pzb0|GD#R-tKv;1%LgEzO_9y%)Q`cTJiT!3vTUw%@?(^wEz6P z_}+{s)%&K+Tv3*PyY*V@zDX1FBM!!I|LB+U<-)8B$-95;%KGP?{uLd z284gA@ZFdE^q>tJoVjoIs+uCjFH)Vi zqs`a)+|PZs(e;J1`1T#=BBwUaJe}Ngp!lvI6;>1s;*b z3=DjSL74G){)!X^239Li7srr_TW{}r=T8ZhIsP$z=axG@g09USES^1W86V`r)sBhk z#q2P0GvD=)^@mms<385wdJz*Ul9eYa+~Y`C>7=RP)ac~>zUO&Kl9PwZ(QPGq^Zh?D zv`*sr|GDP5jq%)t*-Rxi3!5)6E@XCMZ{g#R6|hn$ad-j34akBp3Amt>ITJ)FT!cdw zq5nWp%fHCQQ}Yv&{};z@Rj{2uzaVVe6#wcsSWNxGXJ|h6&aQbrJ!+SePhP1oGM;$G zZ|dSdk;&_RR&F*l&+tt^hMM>)=6&f zjGov$xyi}g|96{yo06RNX}^7XY-io&YtJt3Uwel$$E3LBpM%d7+25}Mir&T)KacbK z+P7FP{9jI8O!=?&a+Tbv|BH7| zCdQugXxXdhu7?N)90CNG8g~kr1#9D z?8)pm=TdIIbbL|1s(=4^*=#$x{N&YMAJ(U5*!^g(R6D-uu4MOx=lieKJ}W4C9OV9Y z^R;xf_s=Tk=^w10X}dG7j{RN!60F_Tj_nWzUuzw_vgPgA1=gNS3NE(`sPto zxj|cOjsE6J{rzd->z+Ey)0JOaBzF1X!t2wQ@X2@`bKjNsFz!@=-1)OL6ZbwnZkKK( zTlF$7KBwqWLb%(%^=~E5s{d5%Pdzi!hrKQC@w$oE)$i|$saN|{*!lNlkJap#3og8# zoV$FV*R!-W_hkET+n>7lX~*AHiNJ((eeRzx<*J_|wacg96Z-wzGA(`iS?BfdO{~9e zJ@H=hcZ{8m_3qQy^o!NZhwe^l?_wCR9|CDsjJiX&W^w+lB zlWX6c_SD|uQQB5IjbpFs)$;7C`jQ(hAl#>WCOv#}#&q>&msdKkeFUpc-hhF`zltS&m)rL);8d3~$+udYhv^taFWGs4$zUUvJM?2C$hv$ZfY6H?X#!lvI6;>1s;*b z3=DjSL74G){)!Z!8bePP$B>F!Z|~mq3JH{Gc(`<`=*3JI5s{J$iRRZsONt!>gcfR~ z^+iq#p1Iiga?}q_VWn--<(up0vD+zmUE2EHI=k+h&voGol}_vnnVr~M_&8(*tQ1Nd zUNl^A1Y$;HL6`(wuqBTZq!7*rDL_(G+VZdH)|>o~mFM|RYNw|z-V?EDF^U}!dv+&H zN*5`f^xWdS#j-y)OP;;geID5rzy9c*j;FDmk=vGV)PD3j({AW4ST+k+EcZO)XlGFZQyPq7Magk|%|K@hq--5sH zN(lU#WBqD7$DYjm8`=l!&wW-sA2pA?GSAXluxjgp{caC@!}A)-lHNV7)Tn5w`x*P? z?~S9kGtAE~xB33r#%tal=NHd;91GANrH0x8G~86AF}B_U>=Y`^V|k zv;5+hHSfQ^aJ9v$(_zuFJvCvK54BC0{L;liTHJwfDmF$fAS1vin}g#Z~{iaQQ^K{JK5oK3|^cBkrtUn7{n8`pRGL zG?L__xORzDRc>0l=ZQ(-mwy!;pVhX0pT*cxm-qda;Lp~L>u=r6tbVch#>wS)TzMF6XrV&6{4ic@?X_38Y`;RnFzA)&KfibxVC{TvhaDo-K-B zKJn@K>z&!lvI6;>1s;*b z3=DjSL74G){)!X^2IeSF7srr_TW{~2)p9A6IPlQ;l7yG?Q6|-G!e@V+@jBVHD8h5f zjWefCh3;S|e<(I(?NsTF+aB9mh9{)Wd0anl`MtgO_Q$91J12R*;R&Z~3m=E9fR#dt z!;6Lsj0>5a*g+Us&=Dp96BO7b3RMadad?5G-%aq>80J%0&2NsxZTh@tntF-`up?hmG&orbex^R9bVQF*MDREo^$;*E~G&ljlsc&We&3Kkr8QJ~!Tw zw$ZyK&NABY^trT*Y29;`zMPuTWm%%d16=J&~?(k8*^rvvE!lvI6;>1s;*b z3=DjSL74G){)!X^1|~aC7srr_TW_yk;{d9$L^&JP5Rk7?eGW7k)#nWtFr4e~f{^1d zLzEbEk-`ra_Q;U}4ltMm%mo-Rg~h!X=3;mm)4fo0rL?vhu{}%KobVc$ycj%P{an^L 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 zcmb7`dsNcd7RTvjB>AY(Oftz#TV{z`X@*FmjinKdTB-Ob&73kNlzgI=lGY@0)zlsu zLj_i&lD7y;$;z%&KKM;$CW1|nN+}wFA`fx&I(MeK?mu(?_@2Ga-g|x5K5Lz`KIi z+cp6Nz-5fU8~)riHa3Yr2j9Pc6XWRX3$`~lE|RTl&EfF*+K`2I#r52Kn-N;|E)#n( zRW{ViqFwIo9slqqi5sP3SG4XL@1q@DVlZ;o0YqPbUQ9LyuQuJjhPXp|8t|Ucyl<2L zDg2rIw)>CdUlxaeI~OCz*xCS6Y3~%d9(*!vdA7C9Zca{cG zfr51@!;52Ux^)lVwlzfQtr~Si%Tnv};H)nNAKPMUwoaZJsa&z~A`tfqM2DU9i`aOE zBU_ZYOegYu{7O>l**PRH^jnX9kNZvK|676u>U3fS!JZ1q^^-QjuEQ@zkjz9cC2gLa z19DQ5+?b9!`q0V{PnlFLL+C8*CShd})~#Mj%JP5tf9_X}6V&}q)%q^t7ttxHQ&F1W z^MS>-Il-0r&vBTJ%#W^#hzyqJmkQpImttn+beR-xmnw8Ip!4#)>h`gUKD_Q==qX-s zdv_u5!AF%gs#dsAbia0$25bBi21^gad}Jh!FD%Z%melJq(0R=WopCP=i7WSjY@j*hlv9sw>ymm$;^BQ&hMVv|oy_Hje9lX5ikr5QMD<#?y_D1CB5GQ z<|e{Ja$6(Z7NJ>vI|G1i@y-Uung3WKGJE&yuqO&tjM{_CZN*fucSzaW;x)LIFfiAx zZsI`!`(#n%)gWf`)|#_GyhN>-xVr+)>L~n#H;T7W3amCy)gInNj=bv7^&a{bEv?cC zLm-f)v#$(r!}MEn4*y~0IOl+DAQu5De6Ny|&z3gW!&^mj2BS~yh~&pPse5wbLm>2? zqn&dNtxm1`r&@M4-)c-ZaV!``vwGvoeyz;C-PyTDqciwkA%`fagp;30x{#96P#91L%;{ zO>Hh)aIu}sH2YG=8@}p`2UcF8Zs4bYxi({=#>p6-4{XlJgvb>C0FbMNey%onu>`_nqA?{}+lk0jS|LMW-b!?_SEMRZoS*AFGDR&f-e z!#xoZtTc0hxvFmSV|u~um!$q|7X|{Nt{G4CzgpERk{EN+D0*$=*?hpqihm8+jlwsV znGB3MRliyF>3xtXzIOb9J7tFyyBy1n*xp0Ub{iX2Sr>+Vkry?LWl-J8`Uoq;mDd9b zbyL<>#zWGs>HQZ{Cy#5t3Q&qPKF-xvId!S?>GZa-FBW^Nw=`ek1^WLoESnUFk3H++ z4-(eyKiEY>Zd|7E#th=sI+Z3S-Hvg#sRb^?9!rRsjN;vvq_-<+(1rA#b@ML}!lvI6;>1s;*b z3=DjSL74G){)!X^2G*UPE{-7;x8B}8-yNDNao}Td(F_xA-CvB7o`KwxlVpB3u2zVd zd1XB8+PbqrOS=Zp)lJzjJ4=jDhGm-gO$x<}yoUJLdf=7rmH z-RFcq-Xog6y-?xHHe0!L)pM8K8ZX=~OP_Q7(Vj~?4>nzxyuzrOEp3jQ*QJGTGgPU(K2 z5!n@z?~BV??(JxQyeEg}kGxa-run*-32j&8s;Yh*68wGBh=0?9R~Ih(XY1ZNKTVf! zv&)OCm1gJScF&x>_ppy(mFc^#;u~?#-hD8LZi#E1{wQbr^qsfu&WJ4(|L!oSxopze zgNesD_O1}$eVgA+rN3^5p_9H^t$p?W+LZ14KgU)q6fZe(wPwreUy8{yp4{u6U3&NK z$K$^XxBY!>zi!T-f*@(jvM-AT|4rNfxJKyo|5&DVFOs8voj-9hHTwDOjZX4Ev*zhU zAAK<`{hlsot;@@W8vFWe_nCaQJ*MzwqfILB(H{$(UpUQgsC85LVt8-<{rf*llT_c% z`?+b(1@{k2L`A;02y)nNcVD}}!g@vn>;Dric`WN*Sl*w%ynX7sElrQg_;x$Ih_p%h zb9Dpv!t}>^`L=HkRe#BOXZkPeM(c&mAAeTbzHxeS^T*eOuV4iWmw@TbhkvY26nD!1 z^!}Khjsz&xEihBvFIj#WEPVbM|Ks&-u^e~p^%g3BIs5x8NVHeG()e9Ciug&WYIl&R z$+{b7cl`Ty`snMhfIDCBd6zFt|29`tB%R&q{JUsvjdOf0^X^^`3z#FzVf*{{mlEL@ zt4u|Tx0WmWy-?nn!kgy*dtv+*w$01`UbuXAfp(bTF73I}g--GxYk%qdv&a`+Y;Au= zZejnk(-zy;B%5q+o2mRIaQ&;7=YF%kmN?sbZ@H|K{*yJOGfaEi<-%v2p4(r2tRQoS zsgwS*o~bhoL4vd7ANBKQpG->zMenqZSmA+`Ld-c+>H5i-uhYE<(cByg00_MRThRz{XYNkxTf~4O~HqCW^TAufAY~T z(cF`+#!G%(c{0(O&m5j#SKIz7YR==m{hESRoBwrvUg>V3{H5%!jMD#m zz)Y}vV!hbJ`eb0re>?rPlKYF^UC*9cIps(H;&{75I-g^2?90;z>NPEGz&x0@Yw3En z^}mgHobrQzX>JLDkn=JPxUmh23ku6{1-oD!MDw?M0v`tx& z7}#dkh^d*Vk(*@ZfufcuUJ0w=l6e4Xg1A~a)y_7}{_(ueyx;FL-{+5a=JTcn2VzW( ztc*Y)kg30)&z|`{vrq=bh6NwB!f6 z69EG0f&6`tdryl-dV^9!LSTQ6lgUPuEp>RSni4Ja3-hhumB_+Sos+D@I|1pToD;3C z*ed)mD}XN22}ppV!7fY_ib9sC_ubB~$_a>@&yP=qIZnwWWAV&kB|>wNd)Cr3Thu*M<5*Tr?KNq|c{6 zh{#V78Q4`d8f2H37~=u0k85ctVZ>jC|Ia{M3KsN@tp{~O485M((k_e-@Hic_s@#$8 zm@$c`X+(d5NA8xfx}Nj0_U-E9%cbX3M(+@eF#BJRm2MnGponkqj3%hBWJo{x5i8FI zqo@-D&%d|Io{YUm00TcoS)z|eb!#z=xvD6U7Sn|kr#TgS)hVT;>8WLtNRKHTISR-3 zMqzCgl?3O+j%&zeyQse#_L^^J38`_95uUrCNZD=q1~A|-bRyoV2_-Efm6gMe48XYC zupfnS4HdCqbVaDr51FHi_neG7kvW610kqKA4RJ~VS+5B`;HsSpy1`dUh-ixmPIGw&Q-AXD9%ea@~2+7X4 z`P6!rT~s(e$I^fsRc(coHF-*R@l~*4qPEFbp?QKwn*aU ztC+dl^kRah&_R$SwuP^RtpgF$`d(3IY&~ymI{z%^c}$i7Do5kWFXTCu%NcORj;z0j z8&viZd{aR~l{Jc3?w+GgJvYTPEPd}C^m8L%6pR3v03zk>Qa zTQf3%8r1ejIc}Ns67$x;Lrc+AM>C+02m5}&arSBl&$Mx$J5j!(y7&L>ZlK1>XxdAgQVLvS}6 z+mg#$tm}ty(me9yl;rxzl)4Rz&qdhfmon{!kE!)ZN(FjrKKU;;TVB0hQ@Ibwo#k^F z8M3O_5)o;bg`L5Vv6k@!bipMBE#9+E6ChQ{Iu%&Fv1e7FBiiN~|Mkdl^SNP&$W2jv ze^8xE=^-*(RRRoV3L*j4n-KFS!mmyf?fuF-T;JR_GdQD57Xuz_NX^*E)JQwWX$qz% z@9lVsV6-NW#I;T-i?vC~zPt0x2)$p+-w~BrvCbKF;P4pnsq&ab9Ax)QN%@|~T_iG2 zPV#VZS5AxCmotl6lStgAuO+2B^j?#rM0ybJF~+`ghrvcw>11p4e(u&P+Vyt ztbonE<6L>BeoaV~jl-0aoFVrqFd!eA0PN>x2Vrx6sD($v8t1e*t2Hsgh6fjAWC2ui zy@$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-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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 9d505793..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 17360edc..00000000 --- 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 65/86] 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 c8892b87..00000000 --- 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 00000000..ba812bb5 --- /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 9a791fde..00000000 --- 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 d60adda7..00000000 --- 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 ee0f3d4f..00000000 --- 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 55715e2a..00000000 --- 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 3d61682c..00000000 --- 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 00000000..be3036ed --- /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 00000000..f3165177 --- /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 00000000..d330434d --- /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 00000000..c555afc9 --- /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 00000000..3c11beb3 --- /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 66/86] 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 6d230ee2..9a8b45be 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 652850f5..00000000 --- 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 c622d0bf..00000000 --- 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 646002b0..00000000 --- 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 a3d1fd99..00000000 --- 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 4431a489..00000000 --- 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 6ea570a9..00000000 --- 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 00000000..c4e08730 --- /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 00000000..c4846cc2 --- /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 00000000..da2cce58 --- /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 00000000..79e70fc5 --- /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 00000000..69f16cb5 --- /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 00000000..87b5da52 --- /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 67/86] 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 1d208a0d..a29b3267 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 43ad525b..67f3c52f 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 d1881074..f05526c4 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 c77648fe..e605ca4f 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 26e151dd..3fb1e24c 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 31e15603..0748409c 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 41e6d410..fe2f4332 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 378b0f53..25087a42 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 32e594f8..7393b861 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 1e25f761..7aabda00 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 47329393..66cb782f 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 6dd59fe2..a17cb353 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 462ffcfc..91a2a83c 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 8cc405e4..fd628717 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 f3deebc6..e050f7ff 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 68/86] 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 3b8ed47a..87063f20 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 00000000..b06cfb14 --- /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 00000000..20306876 --- /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 00000000..93de85cd --- /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 00000000..45adf807 --- /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 00000000..e0579d33 --- /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 00000000..990cd474 --- /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 00000000..2c0a3d92 --- /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 9993d5d5..00000000 --- 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 f61f6ff2..00000000 --- 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 c1a2333a..00000000 --- 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 7fea71a7..00000000 --- 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 429f4440..00000000 --- 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 00af7f35..00000000 --- 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 cfbbe58a..00000000 --- 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 69/86] 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 70/86] 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 d944ca61..914525ea 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 46017f65..b24c3472 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 d68fe1bd..33d3f8ef 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 24acb55d..efb2a587 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 d6518878..9433427a 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 b13d694d..5b46fff4 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 874e0d50..1ea69c2f 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 84033c93..e836e72e 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 d7764d16..08a73286 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 a9af2482..b75804d8 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 cf690043..5c4c1774 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 f747c34a..8b6f68e5 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 1af1d92d..e7a5d8d1 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 47e1947b..d50eac27 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 353dd327..d3e5b6e7 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 7c504510..701efcbf 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 0f94e996..05629b29 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 9c1c6a7a..9d052627 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 312bf9c4..d8416d67 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 ec1d63e7..9aa616d5 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 6a980231..096f34c8 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 6a980231..096f34c8 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 6a980231..096f34c8 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 b46e34bc..5787f6dc 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 189e5a0e..812ec0e5 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 7bb7d286..1c35d054 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 6b10adb5..0f158748 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 ee46aaf5..cb140a04 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 761b4c89..ca9a13ab 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 6b10adb5..0f158748 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 ee46aaf5..cb140a04 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 6b10adb5..0f158748 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 ee46aaf5..cb140a04 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 6b10adb5..0f158748 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 ee46aaf5..cb140a04 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 0848b759..9083163d 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 63d19c05..30e91831 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 f7603337..d1ac5ac4 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 0848b759..9083163d 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 63d19c05..30e91831 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 0848b759..9083163d 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 63d19c05..30e91831 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 0848b759..9083163d 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 63d19c05..30e91831 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e116823e..953f328c 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 e6eab74a..79cd7f67 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 0848b759..9083163d 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 63d19c05..30e91831 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 63d19c05..30e91831 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 0848b759..9083163d 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 63d19c05..30e91831 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 63d19c05..30e91831 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 f7603337..d1ac5ac4 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 0848b759..9083163d 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 0848b759..9083163d 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 ee46aaf5..cb140a04 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 9dea11fe..3fca4d70 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 da321f0c..5273f1f6 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 97f49f60..95f4e5c4 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 084ff1bc..2754dfdb 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 22d5914f..f605fcda 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 02c03224..49fcf0c5 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 aaaa001c..35b66a73 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 8f661c05..d61a0f20 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 09881b5e..b5724b84 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 02c03224..49fcf0c5 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 111778ee..76673a63 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 2a045317..9d2e596e 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 cc67c91c..1ab310b8 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 f30bc174..e55378a7 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 bb11e621..7333d02c 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 1c6c67d9..20e8f35a 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 0ee00c74..f59ea17c 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 cb9431bb..9ab6cc77 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 0ee00c74..f59ea17c 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 e94068a0..2205dcba 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 aa07708e..1c55a589 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 1b1953c5..adef2c3a 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 442db9eb..9d505793 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 22d5914f..f605fcda 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 02c03224..49fcf0c5 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 111778ee..76673a63 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 8f661c05..d61a0f20 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 09881b5e..b5724b84 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 8f661c05..d61a0f20 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 aaaa001c..35b66a73 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 2a045317..9d2e596e 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 b524d9dc..2d42e2a0 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 b8535b73..17360edc 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 22d5914f..f605fcda 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 02c03224..49fcf0c5 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 aaaa001c..35b66a73 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 8f661c05..d61a0f20 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 09881b5e..b5724b84 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 02c03224..49fcf0c5 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 111778ee..76673a63 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 2a045317..9d2e596e 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 cc67c91c..1ab310b8 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 cbfd0127..c05007ce 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 22d5914f..f605fcda 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 02c03224..49fcf0c5 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 aaaa001c..35b66a73 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 8f661c05..d61a0f20 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 09881b5e..b5724b84 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 02c03224..49fcf0c5 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 111778ee..76673a63 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 2a045317..9d2e596e 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 cc67c91c..1ab310b8 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 bb11e621..7333d02c 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 f30bc174..e55378a7 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 bb11e621..7333d02c 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 1c6c67d9..20e8f35a 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 0ee00c74..f59ea17c 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 cb9431bb..9ab6cc77 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 0ee00c74..f59ea17c 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 e94068a0..2205dcba 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 aa07708e..1c55a589 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 1b1953c5..adef2c3a 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 0ee00c74..f59ea17c 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 22d5914f..f605fcda 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 02c03224..49fcf0c5 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 111778ee..76673a63 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 8f661c05..d61a0f20 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 09881b5e..b5724b84 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 8f661c05..d61a0f20 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 aaaa001c..35b66a73 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 2a045317..9d2e596e 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 b524d9dc..2d42e2a0 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 8f661c05..d61a0f20 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 50c109be..3f7a7ae6 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 420694cd..9db4dd1d 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 f664fcd7..483091b7 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 c12ab560..95806e72 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 3de5762f..62efbae2 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 7ead6c60..eadea109 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 e072cda8..5a7d0917 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 6424b742..6489d53c 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 30e9f56d..a8cc5540 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 969d80f9..56687b97 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 be3036ed..0a684449 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 f3165177..87bd10fe 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 d330434d..3a0c39b2 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 c555afc9..4b8e518d 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 3c11beb3..3d61682c 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 eaff6def..ba487fd7 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 2aadc6ef..60af1f39 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 dade3449..7f1c0cb0 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 84836faa..9a7f7901 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 b3859ce0..368f44ff 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 a10f7de6..368f44ff 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 f4d76fb0..b213ccca 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 ce158e06..b213ccca 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 1e09fdf4..a37ebfa7 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 17f5c20a..1b997c82 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 886c5059..4101cdaf 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 2789f97c..e8f8954e 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 1f446a1a..033d08f8 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 8ad912ce..7f6128e5 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 1c9bc57a..0632d49b 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 09abafc7..fdcb3ce7 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 0705678b..2bbf451e 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 f2128189..4dbc02b7 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 5b98c71a..fb196598 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 1f5ff275..05eea0f6 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 5c9d0cae..57d0f71c 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 5cac37f4..b47df440 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 ae5f235f..1941534c 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 1ae34ed2..494270cd 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 0c599000..a0734ee4 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 2f38a950..4ea595df 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 5c9d0cae..57d0f71c 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 aad3872b..50fc6329 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 8a06ad3b..bb7c6e05 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 1abf93c6..d6383b78 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 2ca11b59..cdfea92d 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 4470c579..9c9a12a7 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 5cac37f4..b47df440 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 e0b5752c..21eeb373 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 d3c717eb..62a24e74 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 71ac1375..996b694a 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 ae5f235f..1941534c 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 15a94655..3dcb40ed 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 97603b56..fa021944 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 ae5f235f..1941534c 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 1fd9d970..5510cbb7 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 ce3f363d..f6793dc9 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 71a0bc8f..c3a12933 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 a0fe867a..74465715 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 5acd7f8f..1fe5826b 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 be90717e..1fe5826b 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 7f87f9d4..109bad9c 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 7f87f9d4..109bad9c 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 1909adfc..bcb3f384 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 ed9c7271..1999238d 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 6fb508f1..2989aa05 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 7eb5bc6d..348ab784 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 382323e5..714585d9 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 ee0dd570..8dda79f3 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 ee0dd570..8dda79f3 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 ee0dd570..8dda79f3 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 f2d7da91..86978ce0 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 dddebf3d..42b48642 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 34978f1e..72d55008 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 c512f35c..ba25a696 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 5f7077b4..d62d7aec 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 c1be2838..3d75bf3e 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 1348e785..1ffbe1b0 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 c024c490..842796a9 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 1fd9d970..5510cbb7 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 f93de56a..cf385db2 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 28c0a0bc..99f68c1f 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 28c0a0bc..99f68c1f 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 4d9445bc..30cec996 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 2588ada2..9026bab6 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 2588ada2..9026bab6 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 738f02c6..ae3660f5 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 48b5a132..ffe949b9 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 48b5a132..ffe949b9 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 738f02c6..ae3660f5 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 c86a8c7f..8d4a81f7 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 c86a8c7f..8d4a81f7 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 bfa7f3be..772cbc72 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 bfa7f3be..772cbc72 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 b6abcd26..435275e2 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 b6abcd26..435275e2 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 bb6ada2b..cc8710ec 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 a372ec97..f7d16240 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 68dea434..bd35802b 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 38361a37..5af54eab 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 d7037317..3823ec42 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 e9c92bb5..66efecde 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 50bca02e..bb9d648d 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 501ddf10..2ce61508 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 f334102c..8fa401b3 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 9823b0c1..731c7682 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 428df318..96553c88 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 7274161b..fbca3605 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 d86b8959..dc2a9cab 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 197e0a11..4e26095c 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 666fead7..d029a873 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 666fead7..d029a873 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 666fead7..d029a873 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 666fead7..d029a873 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 0f374410..b5a207f2 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 0f374410..b5a207f2 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 9f0037f8..85bc516d 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 9f0037f8..85bc516d 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 9f0037f8..85bc516d 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 9f0037f8..85bc516d 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 c878a2d6..317d4326 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 c7c963e0..0945fc43 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 fa919bbd..41994df5 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 5a722356..d24bd6b8 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 8ab2e075..ad5b15cb 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 81a10570..43bc5029 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 ab622192..7a434207 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 ab622192..7a434207 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 ce8a6b4e..15431f30 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 b54d96c7..4e29cc25 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 61319dd4..3fe215ed 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 722981db..8ad422f6 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 5080ee1f..c7cb0018 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 fbb6127c..5a79a3c7 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 66151f9b..8fdea2ff 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 66151f9b..8fdea2ff 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 a1276627..12ecc129 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 a4287c10..d6c24cbf 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 46efe9db..7c67a4ec 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 d39599e3..7cd33f33 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 c10fe137..5be0df23 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 83cca969..debdd636 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 d78c4a20..dde4f741 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 2136151b..28349755 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 5420e25e..37f18107 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 36e469dc..a6628377 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 c2c16420..50e6559e 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 88b563c8..c6d01e64 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 8b11acdd..ccd51547 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 ae98e58e..29f91d6e 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 2cf1f58b..b6b082da 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 3f00c233..2234ebc2 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 4fd81df9..cf2790c3 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 bd94e0ba..7631eab4 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 0449c3a7..0878f254 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 5c94cdb6..509f92aa 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 abde514b..3677ec83 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 384c6ec1..ab37337d 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 b34e8deb..ea923d34 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 8953a1a8..2de4910f 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 70d555da..28988a33 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 a3c036f7..1c981f99 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 19242c79..c7be2c3f 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 2c8dd12b..85315b4a 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 c4e08730..c5a94188 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 c4846cc2..4ba7d7ba 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 da2cce58..087e3258 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 79e70fc5..911950d6 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 69f16cb5..5025bff9 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 87b5da52..a2e70e54 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 cebcbc75..997b6720 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 d108058d..1b7e4c6b 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 49875359..265e0f2b 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 612d67db..92538909 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 4bde3c32..c48efa24 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 19c8c211..e07ff494 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 4406ac4f..2ec013d5 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 9bc8ba0f..266a6d6b 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 b06cfb14..3c27e680 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 20306876..9bdb776c 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 93de85cd..a0691891 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 45adf807..ab555b0b 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 e0579d33..c19affb0 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 990cd474..562c76e6 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 2c0a3d92..613ac0e0 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 00000000..a2a98cf0 --- /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 00000000..09a15b69 --- /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 00000000..09a15b69 --- /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 00000000..dd5a2ace --- /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 00000000..70a20754 --- /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 00000000..70a20754 --- /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 00000000..39f29033 --- /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 00000000..7c6d73b7 --- /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 00000000..7c6d73b7 --- /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 00000000..1a905ff8 --- /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 00000000..1a905ff8 --- /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 00000000..1a905ff8 --- /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 00000000..1b1ed3e3 --- /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 00000000..1b1ed3e3 --- /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 00000000..1b1ed3e3 --- /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 00000000..0d51f838 --- /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 00000000..0d51f838 --- /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 00000000..0d51f838 --- /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 00000000..00a793ec --- /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 00000000..00a793ec --- /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 00000000..00a793ec --- /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 00000000..443c5e78 --- /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 00000000..443c5e78 --- /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 00000000..443c5e78 --- /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 00000000..d835b86a --- /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 00000000..d835b86a --- /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 00000000..d835b86a --- /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 00000000..0627f844 --- /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 00000000..0627f844 --- /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 00000000..0627f844 --- /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 00000000..71d06c28 --- /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 00000000..71d06c28 --- /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 00000000..71d06c28 --- /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 00000000..d8b6ebb1 --- /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 00000000..d8b6ebb1 --- /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 00000000..d8b6ebb1 --- /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 00000000..212ff2e1 --- /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 00000000..212ff2e1 --- /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 00000000..212ff2e1 --- /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 00000000..bcc59e5a --- /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 00000000..bcc59e5a --- /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 00000000..bcc59e5a --- /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 00000000..ff359033 --- /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 00000000..ff359033 --- /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 00000000..ff359033 --- /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 00000000..c561128e --- /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 00000000..c561128e --- /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 00000000..c561128e --- /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 00000000..43394d29 --- /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 00000000..43394d29 --- /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 00000000..43394d29 --- /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 00000000..31b07cb5 --- /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 00000000..31b07cb5 --- /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 00000000..31b07cb5 --- /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 00000000..d835b86a --- /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 00000000..d835b86a --- /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 00000000..d835b86a --- /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 00000000..1ad01578 --- /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 00000000..1ad01578 --- /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 00000000..1ad01578 --- /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 00000000..9f6074d7 --- /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 00000000..9f6074d7 --- /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 00000000..9f6074d7 --- /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 00000000..c76cbf48 --- /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 00000000..c76cbf48 --- /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 00000000..c76cbf48 --- /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 00000000..b9ac5f19 --- /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 00000000..b9ac5f19 --- /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 00000000..b9ac5f19 --- /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 00000000..55694d40 --- /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 00000000..1eeb0177 --- /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 00000000..1eeb0177 --- /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 00000000..516e2f40 --- /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 00000000..516e2f40 --- /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 00000000..516e2f40 --- /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 00000000..883df563 --- /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 00000000..883df563 --- /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 00000000..883df563 --- /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 00000000..55a94640 --- /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 00000000..55a94640 --- /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 00000000..55a94640 --- /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 00000000..883df563 --- /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 00000000..883df563 --- /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 00000000..883df563 --- /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 00000000..096f34c8 --- /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 00000000..367e87dd --- /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 00000000..367e87dd --- /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 1770f151..cf5b2640 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 a60e0771..6984eb70 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 5bf6378e..4eb87dfd 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 61df79c3..9c07514e 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 d5630da2..cc11924f 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 66cb782f..956f6473 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 a17cb353..ce83d58c 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 91a2a83c..9433427a 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 fd628717..c23244fc 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 e050f7ff..5b46fff4 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 5d808e14..1927b2a4 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 5d808e14..1927b2a4 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 381ff3db..a57a541a 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 f2d51399..209a13c0 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 74de36cb..a56e0e6d 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 464b8f0b..69e8495e 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 3595cd1c..81383beb 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 464b8f0b..69e8495e 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 3595cd1c..81383beb 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 211f4f24..c3ee1381 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 71/86] 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 fd92011b..00000000 --- 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 04497dc9..d5fd9309 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 c6e02256..00000000 --- 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 4ea3bbf0..d98eac94 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 4d4c6673..cd06ee8c 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 6813c8fc..00000000 --- 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 36505d90..00000000 --- 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 0654942a..00000000 --- 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 10ac9ba3..00000000 --- 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 707e53ac..00000000 --- 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 d4fb0bef..00000000 --- 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 6726e2bf..00000000 --- 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 fd0ed2aa..00000000 --- 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 6ebad1e0..00000000 --- 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 0971db35..00000000 --- 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 9ad68315..00000000 --- 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 86bb20c2..00000000 --- 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 1629bdfa..00000000 --- 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 3b8cb4d8..00000000 --- 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 ef26eb10..00000000 --- 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 6c87e509..00000000 --- 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 f8d8c763..00000000 --- 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 ddfc43ba..c27880c9 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 8e41345a..25b105f6 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 938bd672..130f8796 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 900e4b29..00000000 --- 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 c942b2fe..00000000 --- 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 a6a9bb47..00000000 --- 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 5e4089c6..00000000 --- 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 f4335778..00000000 --- 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 ec22ed8f..00000000 --- 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 01065c76..00000000 --- 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 d44a46fc..00000000 --- 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 cec760e7..00000000 --- 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 5bfe77c8..00000000 --- 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 0eba43ac..00000000 --- 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 23dce964..00000000 --- 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 6ff49426..7faef971 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 82e89c16..b47dbf64 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 21dedf41..201eeed0 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 245d7e85..48e9e95f 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 219c9eb9..122c7cd5 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 f5c35606..9c127224 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 acac46b0..c83b5880 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 60ba9b19..c7e79c67 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 b5d04ec2..3af51156 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 63fc8bcc..b0b250c4 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 45f09159..00000000 --- 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 d7d7bb4a..cba7efce 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 00b538d6..00000000 --- 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 178dfa48..00000000 --- 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 15560240..00000000 --- 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 c7d24985..00000000 --- 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 91d56671..00000000 --- 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 5ab5ae86..00000000 --- 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 8c283ed2..00000000 --- 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 cb104bbb..00000000 --- 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 fbc3cbee..00000000 --- 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 5e5ed330..00000000 --- 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 f4cdcd2b..00000000 --- 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 bf895e21..00000000 --- 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 070f2577..00000000 --- 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 aca2d2e0..00000000 --- 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 bac4ffb0..00000000 --- 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 a13537d4..00000000 --- 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 e7d017c8..08a85906 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 4355855e..00000000 --- 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 f2d71700..00000000 --- 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 fb141083..a3d68f07 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 72/86] 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 c3302113..426e5be3 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 9c127224..015a9f0e 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 6700b36d..77bd27e4 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 dc86d0b2..59010c02 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 7fbdc91f..c6946624 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 53c15901..6b027a7a 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 73/86] Update ImageSharp.Drawing.sln --- ImageSharp.Drawing.sln | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 318aeae9..c7e333c0 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 74/86] 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 60d7a259..625a4da1 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 b127c714..9ceda1d1 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 75/86] 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 9ceda1d1..9553c38f 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 76/86] 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 914525ea..8b624f85 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 c79ff558..8a1fd82a 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 77/86] 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 8a1fd82a..435069a8 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 78/86] 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 8b624f85..eb62a072 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 79/86] 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 eb62a072..7a099bd6 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 80/86] 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 625a4da1..df9168bf 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 510101e1..343dc906 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 2544ff5d..23da79a4 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 af642517..00000000 --- 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 00000000..6cd07f57 --- /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 6a2183c0..d26a4984 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 b47dbf64..6c6b1dd9 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 9a8b45be..cd047bfe 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 88c9f54e..e62d612c 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 19842362..9b905942 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 382d6fa8..eb9a2806 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 81/86] 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 67d4520d..c5c3be60 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 e337d54c..6cfe07d8 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 df9168bf..9cefb806 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 23da79a4..3c4aa61a 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 480d7a63..00000000 --- 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 8ca95585..a91abfd5 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 00000000..fda958fe --- /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 6cd07f57..00000000 --- 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 d26a4984..00000000 --- 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 9b905942..3e3b161a 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 eb9a2806..93fce0ec 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 907d9148..494e8036 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 82/86] 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 b5a3413e..ef44ddf3 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 106c7035..7bc45f90 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 75f5e96f..2d795648 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 b9b3ccde..9ad53bbf 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 e9a44bd3..d655caac 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 1e2e9c10..33a65845 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 00000000..e035fc94 --- /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 00000000..1468843e --- /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 cc6a53ea..1af0919f 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 f1170baf..303482ec 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 78ff4fc1..cd4c0801 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 d423b57a..6f2e36f7 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 dfe11f4d..45482048 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 a3dc7583..a37f427c 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 c5c3be60..c6f6ce23 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 3bb60974..f3973cca 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 9cefb806..1d7dc636 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 3c4aa61a..2ff6a817 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 43865750..73deea4e 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 fda958fe..405c2cd7 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 66a8cfbb..6b627530 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 55678d28..2ceedda8 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 9aafa908..3d029523 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 3e47f6c7..745c7efe 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 ff5a328d..e115d863 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 1592c644..a4fb37a2 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 dc944d68..7a23caad 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 c8e7cc26..00000000 --- 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 6f5c2f40..00000000 --- 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 849c93e7..00000000 --- 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 eade3443..5c495ecf 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 e20ed9eb..00000000 --- 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 b2401ddf..00000000 --- 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 c3a07c11..00000000 --- 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 f85acbe3..5b9dd9a6 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 00000000..7c2aa4f2 --- /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 42d7e598..c21cd931 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 9553c38f..da30dc62 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 d920fa9d..21fbd578 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 aa939136..c682f193 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 7943ac2d..4e741462 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 67cdb637..14e62820 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 81c36548..12885d42 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 a8bc7d8d..13cbe464 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 e62d612c..8e0c6990 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 3e3b161a..2fa41c66 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 93fce0ec..f8c6e35b 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 00000000..4d47bc45 --- /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 00000000..fb6d0930 --- /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 6dae4fb1..7daea40a 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 3ed57216..846c14de 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 3af46a7a..e1c4a5f8 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 9ea049e3..f5d9ca38 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 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 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 3606891a..74710b3a 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 a5dcb488..3a2882ec 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 48541351..00000000 --- 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 20698569..00000000 --- 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 494e8036..455b7fe8 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 83/86] 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 2393ce20..0375d8ab 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 f8ee17f3..00000000 --- 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 84/86] 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 1d7dc636..ef84288e 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 2ff6a817..be5d9ae0 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 85/86] 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 9744e14e..f4873554 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 2d795648..fc80958d 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 86/86] 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 fc80958d..22572a57 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 2b1bc320..95412462 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. ///

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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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